File Coverage

blib/lib/Circle/FE/Term/Widget/Scroller.pm
Criterion Covered Total %
statement 36 157 22.9
branch 0 58 0.0
condition 0 15 0.0
subroutine 12 25 48.0
pod n/a
total 48 255 18.8


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   2013 use strict;
  1         2  
  1         27  
8 1     1   4 use feature qw( switch );
  1         1  
  1         67  
9 1     1   4 use constant type => "Scroller";
  1         2  
  1         52  
10 1     1   400 no if $] >= 5.017011, warnings => 'experimental::smartmatch';
  1         10  
  1         5  
11              
12 1     1   62 use Circle::FE::Term;
  1         2  
  1         20  
13              
14 1     1   271 use Convert::Color 0.06;
  1         13942  
  1         38  
15 1     1   286 use Convert::Color::XTerm;
  1         1803  
  1         25  
16 1     1   272 use POSIX qw( strftime );
  1         4492  
  1         5  
17 1     1   1475 use String::Tagged;
  1         4656  
  1         32  
18 1     1   499 use Text::Balanced qw( extract_bracketed );
  1         11678  
  1         75  
19 1     1   405 use Tangence::ObjectProxy '0.16'; # watch with iterators
  1         26894  
  1         1603  
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           my $is_initial = 1;
192 0           my $needs_linefeed;
193              
194 0           foreach my $part ( @parts ) {
195 0 0         my ( $text, %format ) = ref $part eq "ARRAY" ? @$part : ( $part );
196              
197 0 0         $str->append( "\n" ) if $needs_linefeed; $needs_linefeed = 0;
  0            
198              
199             # Convert some tags
200 0 0         if( delete $format{m} ) {
201             # Monospace
202 0           $format{af} = 1;
203 0           $format{bg} = "#303030";
204             }
205 0 0         if( delete $format{bq} ) {
206             # Quoted text
207 0           $format{bg} = "#303030";
208 0           $format{fg} = "#00C0C0";
209              
210             # blockquotes get to be on their own line, with "> " prefixed on each
211 0           $text = join( "\n", map { "> $_" } split m/\n/, $text );
  0            
212              
213             # surround the text by linefeeds
214 0 0         $str->append( "\n" ) if !$is_initial;
215 0           $needs_linefeed++;
216             }
217              
218             # Tickit::Widget::Scroller::Item::Text doesn't like C0, C1 or DEL
219             # control characters. Replace them with U+FFFD
220             # Be sure to leave linefeed alone
221 0           $text =~ s/[\x00-\x09\x0b-\x1f\x80-\x9f\x7f]/\x{fffd}/g;
222              
223 0           foreach (qw( fg bg )) {
224 0 0         defined $format{$_} or next;
225 0           $format{$_} = $self->_convert_colour( Circle::FE::Term->translate_theme_colour( $format{$_} ) );
226             }
227              
228 0           $str->append_tagged( $text, %format );
229              
230 0           $is_initial = 0;
231             }
232             }
233             elsif( $format =~ m/^\{/ ) {
234 0           my $piece = extract_bracketed( $format, "{}" );
235 0           s/^{//, s/}$// for $piece;
236              
237 0 0         if( $piece =~ m/^\?\$/ ) {
    0          
238             # A conditional expansion in three parts
239             # {?$varname|IFTRUE|IFFALSE}
240 0           my ( $varname, $iftrue, $iffalse ) = split( m/\|/, $piece, 3 );
241 0           $varname =~ s/^\?\$//;
242              
243 0 0         if( defined $args->{$varname} ) {
244 0           $self->_apply_formatting( $iftrue, $args, $str );
245             }
246             else {
247 0           $self->_apply_formatting( $iffalse, $args, $str );
248             }
249             }
250             elsif( $piece =~ m/ / ) {
251 0           my ( $code, $content ) = split( m/ /, $piece, 2 );
252              
253 0           my ( $type, $arg ) = split( m/:/, $code, 2 );
254              
255 0           my $start = length $str->str;
256              
257 0           $self->_apply_formatting( $content, $args, $str );
258              
259 0           my $end = length $str->str;
260              
261 0 0 0       $arg = $self->_convert_colour( $arg ) if $type eq "fg" or $type eq "bg";
262 0           $str->apply_tag( $start, $end - $start, $type => $arg );
263             }
264             else {
265 0           $self->_apply_formatting( $piece, $args, $str );
266             }
267             }
268             else {
269 0           $format =~ s/^([^\$\{]+)//;
270 0           my $val = $1;
271 0           $str->append( $val );
272             }
273             }
274             }
275              
276             package Circle::FE::Term::Widget::Scroller::Widget;
277              
278 1     1   12 use base qw( Tickit::Widget::Scroller );
  1         2  
  1         479  
279             Tickit::Widget::Scroller->VERSION( 0.15 ); # on_scrolled
280             use Tickit::Widget::Scroller::Item::RichText;
281              
282             sub new
283             {
284             my $class = shift;
285             return $class->SUPER::new( @_,
286             gen_bottom_indicator => "gen_bottom_indicator",
287             gen_top_indicator => "gen_top_indicator",
288             );
289             }
290              
291             sub clear_lines
292             {
293             my $self = shift;
294              
295             undef @{ $self->{lines} };
296              
297             my $window = $self->window or return;
298             $window->clear;
299             $window->restore;
300             }
301              
302             sub push
303             {
304             my $self = shift;
305             my $below_before = $self->lines_below;
306             $self->SUPER::push( @_ );
307             if( $below_before ) {
308             $self->{more_count} += $self->lines_below - $below_before;
309             $self->update_indicators;
310             }
311             }
312              
313             sub set_loading
314             {
315             my $self = shift;
316             my ( $loading ) = @_;
317              
318             return if $loading == ( $self->{loading} // 0 );
319              
320             $self->{loading} = $loading;
321             $self->update_indicators;
322             }
323              
324             sub gen_bottom_indicator
325             {
326             my $self = shift;
327             my $below = $self->lines_below;
328             if( !$below ) {
329             undef $self->{more_count};
330             return;
331             }
332              
333             if( $self->{more_count} ) {
334             return sprintf "-- +%d [%d more] --", $below - $self->{more_count}, $self->{more_count};
335             }
336             else {
337             return sprintf "-- +%d --", $below;
338             }
339             }
340              
341             sub gen_top_indicator
342             {
343             my $self = shift;
344             return $self->{loading} ? " Loading... " : undef;
345             }
346              
347             0x55AA;