File Coverage

blib/lib/Progress/Awesome.pm
Criterion Covered Total %
statement 35 315 11.1
branch 0 192 0.0
condition 0 62 0.0
subroutine 12 50 24.0
pod 8 8 100.0
total 55 627 8.7


line stmt bran cond sub pod time code
1             package Progress::Awesome;
2              
3 1     1   43187 use strict;
  1         2  
  1         23  
4 1     1   4 use warnings;
  1         1  
  1         21  
5 1     1   4 use Carp qw(croak);
  1         2  
  1         37  
6 1     1   226 use Devel::GlobalDestruction qw(in_global_destruction);
  1         1306  
  1         5  
7 1     1   395 use Encode qw(encode);
  1         6865  
  1         54  
8 1     1   292 use Time::HiRes qw(time);
  1         911  
  1         4  
9 1     1   530 use Term::ANSIColor qw(colored uncolor);
  1         5921  
  1         524  
10 1     1   6 use Scalar::Util qw(refaddr);
  1         2  
  1         62  
11              
12             use overload
13 1         11 '++' => \&inc,
14             '+=' => \&inc,
15             '-=' => \&dec,
16 1     1   775 '--' => \&dec;
  1         674  
17              
18             our $VERSION = '0.1';
19              
20             if ($Term::ANSIColor::VERSION < 4.06) {
21             for my $code (16 .. 255) {
22             $Term::ANSIColor::ATTRIBUTES{"ansi$code"} = "38;5;$code";
23             $Term::ANSIColor::ATTRIBUTES{"on_ansi$code"} = "48;5;$code";
24             }
25             }
26              
27             # Global bar registry for seamless multiple bars at the same time
28             our %REGISTRY;
29              
30             # Basically every call
31             my $REDRAW_INTERVAL = 0;
32              
33             # Don't log a crazy amount of times
34             my $LOG_INTERVAL = 10;
35              
36             my $DEFAULT_TERMINAL_WIDTH = 80;
37             my %FORMAT_STRINGS = map { $_ => 1 } qw(
38             : bar ts eta rate bytes percent done left total title spacer
39             );
40             my $MAX_SAMPLES = 10;
41             my @MONTH = qw( Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec );
42              
43             my %STYLES = (
44             simple => \&_style_simple,
45             unicode => \&_style_unicode,
46             rainbow => \&_style_rainbow,
47             );
48              
49             sub new {
50 0     0 1   my $class = shift;
51 0           my $self = bless {}, $class;
52              
53             # Map ctor arguments to hashref
54 0           my $args = {};
55 0 0 0       if (@_ >= 1 && Scalar::Util::looks_like_number($_[0])) {
56 0           $args->{total} = shift;
57             }
58              
59 0 0 0       if (@_ == 1 && ref $_[0] && ref $_[0] eq 'HASH') {
      0        
60 0           $args = { %$args, %{$_[0]} };
  0            
61             }
62             else {
63 0           $args = { %$args, @_ };
64             }
65            
66             # Apply defaults
67 0           my %defaults = (
68             fh => \*STDERR,
69             format => '[:bar] :done/:total :eta :rate',
70             log_format => '[:ts] :percent% :done/:total :eta :rate',
71             log => 1,
72             color => 1,
73             remove => 0,
74             title => undef,
75             style => 'rainbow',
76             done => 0,
77             total => undef,
78             );
79 0           $args = { %defaults, %$args };
80              
81 0           $self->{fh} = delete $args->{fh};
82              
83 0 0         $self->update(delete $args->{done}) if exists $args->{done};
84            
85             # Set and validate arguments
86 0           for my $key (qw(total format log_format log color remove title style)) {
87 0 0         $self->$key(delete $args->{$key}) if exists $args->{$key};
88             }
89              
90 0 0         if (keys %$args) {
91 0           croak __PACKAGE__ . "::new(): invalid argument " . join(', ', map { "'$_'" } sort keys %$args);
  0            
92             }
93              
94             # Historic samples used for rate/ETA calculation
95 0           $self->{_samples} = [];
96 0           $self->{_next_draw} = -1;
97              
98 0           _register_bar($self);
99            
100             # Draw initial bar
101 0           $self->{draw_ok} = 1;
102 0           $self->_force_redraw;
103            
104 0           return $self;
105             }
106              
107             sub inc {
108 0     0 1   my ($self, $amount) = @_;
109            
110 0 0         @_ == 1 and $amount = 1;
111 0 0         defined $amount or croak "inc: undefined amount";
112              
113 0           $self->update($self->{done} + $amount);
114             }
115              
116             sub update {
117 0     0 1   my ($self, $count) = @_;
118 0 0         defined $count or croak "update: undefined count";
119              
120 0 0 0       if (defined $self->{total} && $count > $self->{total}) {
    0          
121 0           $self->{done} = $self->{total};
122             }
123             elsif ($count < 0) {
124 0           $self->{done} = 0;
125             }
126             else {
127 0           $self->{done} = $count;
128             }
129              
130 0           $self->_add_sample;
131 0           $self->_redraw;
132             }
133              
134             sub dec {
135 0     0 1   my ($self, $amount) = @_;
136              
137 0 0         @_ == 1 and $amount = 1;
138 0 0         defined $amount or croak "dec undefined amount";
139              
140 0           $self->update($self->{done} - $amount);
141             }
142              
143             sub finish {
144 0     0 1   my $self = shift;
145              
146 0 0         if (defined $self->{total}) {
147             # Set the bar to maximum
148 0           $self->update($self->{total});
149 0           $self->_force_redraw;
150             }
151            
152 0 0         if ($self->remove) {
153             # Destroy the bar, assuming nobody has printed anything in the interim
154 0           $self->_wipe_current_line;
155             }
156              
157             # TODO self->remove behaviour will change here too
158 0           _unregister_bar($self);
159            
160             }
161              
162             sub DESTROY {
163 0     0     my $self = shift;
164 0 0         if (in_global_destruction) {
165             # We already handle destruction in the END block for all bars, so
166             # just return
167 0           return;
168             }
169             else {
170 0           $self->finish;
171             }
172             }
173              
174             END {
175             # Clean up progress bars before global destruction
176 1     1   999 for my $fh (keys %REGISTRY) {
177 0         0 for my $bar (_bars_for($fh)) {
178 0         0 $bar->finish;
179             }
180             }
181 1         17 %REGISTRY = ();
182             }
183              
184             sub total {
185 0     0 1   my ($self, $total) = @_;
186 0 0         @_ == 1 and return $self->{total};
187 0 0         if (defined $total) {
188 0 0         $total >= 0 or croak "total: total must be undefined or positive (>=0)";
189             }
190 0           $self->{total} = $total;
191              
192 0 0 0       if (defined $total && $self->{done} > $total) {
193 0           $self->{done} = $total;
194             }
195              
196 0           $self->_redraw;
197             }
198              
199             for my $param (qw(format log_format)) {
200 1     1   760 no strict 'refs';
  1         2  
  1         83  
201             *{$param} = sub {
202 0     0     my ($self, $format) = @_;
203 0 0         @_ == 1 and return $self->{$param};
204 0           $self->{$param} = _check_format($param, $format);
205 0           $self->_redraw;
206             };
207             }
208              
209             for my $param (qw(log color remove title)) {
210 1     1   6 no strict 'refs';
  1         1  
  1         2206  
211             *{$param} = sub {
212 0     0     my ($self, $new) = @_;
213 0 0         @_ == 1 and return $self->{$param};
214 0           $self->{$param} = $new;
215 0           $self->_redraw;
216             };
217             }
218              
219 0     0 1   sub fh { shift->{fh} }
220              
221             sub style {
222 0     0 1   my ($self, $style) = @_;
223 0 0 0       if (@_ != 2 or !defined $style or !(ref $style eq 'CODE' || ref $style eq '')) {
      0        
      0        
224 0           croak "style usage: style(stylename) or style(coderef)";
225             }
226 0 0         if (!ref $style) {
227 0 0         if (!exists $STYLES{$style}) {
228 0           croak "style: no such style '$STYLES{$style}'. Valid styles are "
229             . join(', ', sort keys %STYLES);
230             }
231 0           $style = $STYLES{$style};
232             }
233              
234 0           $self->{style} = $style;
235 0           $self->_redraw;
236             }
237              
238             sub _force_redraw {
239 0     0     my $self = shift;
240 0           $self->_redraw(1);
241             }
242              
243             # Draw all progress bars to keep positioning
244             sub _redraw {
245 0     0     my ($self, $force) = @_;
246              
247 0           my $drawn = 0;
248 0           for my $bar (_bars_for($self->{fh})) {
249 0           my $lines += $bar->_redraw_me($force);
250              
251 0 0         if ($lines == -1) {
252             # This indicates we didn't draw as not enough time has passed
253 0           return;
254             }
255              
256 0           print {$self->fh} "\n";
  0            
257 0           $drawn += $lines;
258             }
259             # Move back up
260 0 0         unless ($self->_logging_mode) {
261 0 0         print {$self->fh} "\033[" . $drawn . "A" if $drawn;
  0            
262             }
263             }
264              
265             sub _redraw_me {
266 0     0     my ($self, $force) = @_;
267              
268             # Don't draw while setting arguments in constructor
269 0 0         return 0 if !$self->{draw_ok};
270 0 0 0       return -1 if time < $self->{_next_draw} && !$force;
271              
272 0           my ($max_width, $format, $interval);
273 0 0         if ($self->_logging_mode) {
274 0           $format = $self->log_format . "\n";
275 0           $self->{_next_draw} = time + $LOG_INTERVAL;
276             }
277             else {
278             # Drawing a progress bar
279 0           $max_width = $self->_terminal_width;
280 0           $format = $self->format;
281 0           $self->{_next_draw} = time + $REDRAW_INTERVAL;
282             }
283              
284 0           my $title_in_format = $format =~ /:title/;
285            
286             # Draw the components
287 0           $format =~ s/:(\w+)/$self->_redraw_component($1)/ge;
  0            
288              
289 0 0 0       if (defined $self->{title} && !$title_in_format) {
290 0           $format = $self->{title} . ": " . $format;
291             }
292            
293 0           my @lines = split /\n/, $format;
294 0           my $idx = 0;
295              
296 0           for my $format_line (@lines) {
297             # Work out format length, spacer/bar length, and fill in spacer/bar
298 0           my $drew_stretchy = 0;
299              
300 0 0         if ($format_line =~ /:bar/) {
    0          
301 0           my $remaining_space = $max_width - length($format_line) + length(':bar');
302 0 0         if ($remaining_space >= 1) {
303 0           my $bar = $self->{style}->($self->_percent, $remaining_space);
304 0           $format_line =~ s/:bar/$bar/g;
305 0           $drew_stretchy = 1;
306             }
307             else {
308             # It's already too big
309 0           $format_line =~ s/:bar//g;
310             }
311             }
312             elsif ($format_line =~ /:spacer/) {
313 0           my $remaining_space = $max_width - length($format_line) + length(':spacer');
314 0 0         if ($remaining_space >= 1) {
315 0           my $spacer = " " x $remaining_space;
316 0           $format_line =~ s/:spacer/$spacer/g;
317 0           $drew_stretchy = 1;
318             }
319             else {
320 0           $format_line =~ s/:spacer//g;
321             }
322             }
323              
324 0 0 0       if (!$drew_stretchy && defined $max_width) {
325             # XXX this needs to account for ANSI codes + Unicode double-width
326 0 0         if (length($format_line) > $max_width) {
327 0           $format_line = substr($format_line, 0, $max_width);
328             }
329             else {
330 0           $format_line .= ' ' x ($max_width - length($format_line));
331             }
332             }
333             # Draw it
334 0           print {$self->fh} $format_line;
  0            
335 0 0         print {$self->fh} "\n" unless $idx++ == $#lines;
  0            
336             }
337 0           $self->fh->flush;
338              
339 0           return scalar @lines; # indicate we drew the bar
340             }
341            
342             sub _redraw_component {
343 0     0     my ($self, $field) = @_;
344              
345 0 0 0       if ($field eq 'bar' or $field eq 'spacer') {
    0 0        
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
346             # Skip as these needs to go last
347 0           return ":$field";
348             }
349             elsif ($field eq ':') {
350             # Literal ':'
351 0           return ':';
352             }
353             elsif ($field eq 'ts') {
354             # Emulate the ts(1) tool
355 0           my ($sec, $min, $hour, $day, $month) = gmtime();
356 0 0         $month = $MONTH[$month] or croak "_redraw_component: unknown month $month ??;";
357 0           return sprintf('%s %02d %02d:%02d:%02d', $month, $day, $hour, $min, $sec);
358             }
359             elsif ($field eq 'done') {
360 0           return $self->{done};
361             }
362             elsif ($field eq 'left') {
363 0 0         return defined $self->{total} ? ($self->{total} - $self->{done}) : '-';
364             }
365             elsif ($field eq 'total' or $field eq 'max') {
366 0 0         return defined $self->{total} ? $self->{total} : '-';
367             }
368             elsif ($field eq 'eta') {
369 0           return $self->_eta;
370             }
371             elsif ($field eq 'rate') {
372 0 0         return $self->_percent == 100 ? '-' : _human_readable_item_rate($self->_rate);
373             }
374             elsif ($field eq 'bytes') {
375 0           return _human_readable_byte_rate($self->_rate);
376             }
377             elsif ($field eq 'percent') {
378 0           my $pc = $self->_percent;
379 0 0         return defined $pc ? sprintf('%2.1f', $pc) : '-';
380             }
381             elsif ($field eq 'title') {
382 0   0       return $self->{title} || '';
383             }
384             else {
385 0           die "_redraw_component assert failed: invalid field '$field'";
386             }
387             }
388              
389             sub _wipe_current_line {
390 0     0     my $self = shift;
391 0           print {$self->fh} "\r", ' ' x $self->_terminal_width, "\r";
  0            
392             }
393              
394             # Returns terminal width, or a fake value if we can't figure it out
395             sub _terminal_width {
396 0     0     my $self = shift;
397 0   0       return $self->_real_terminal_width || $DEFAULT_TERMINAL_WIDTH;
398             }
399              
400             # Returns the width of the terminal (filehandle) in chars, or 0 if it could not be determined
401             sub _real_terminal_width {
402 0     0     my $self = shift;
403 0 0         eval { require Term::ReadKey } or return 0;
  0            
404 0   0       my $result = eval { (Term::ReadKey::GetTerminalSize($self->fh))[0] } || 0;
405 0 0         if ($result) {
406             # This logic is from Term::ProgressBar
407 0 0 0       $result-- if $^O eq 'MSWin32' or $^O eq 'cygwin';
408             }
409 0           return $result;
410             }
411              
412             # Are we outputting a log instead of a progress bar?
413             sub _logging_mode {
414 0     0     my $self = shift;
415              
416 0 0         if (exists $self->{_cached_logging_mode}) {
417 0           return $self->{_cached_logging_mode};
418             }
419              
420 0           $self->{_cached_logging_mode} = (!-t $self->fh);
421             }
422              
423             sub _add_sample {
424 0     0     my $self = shift;
425 0           my $s = $self->{_samples};
426 0           unshift @$s, [$self->{done}, time];
427 0 0         pop @$s if @$s > $MAX_SAMPLES;
428             }
429              
430             # Return ETA for current progress (actually a duration)
431             sub _eta {
432 0     0     my $self = shift;
433              
434             # Predict finishing time using current rate
435 0           my $rate = $self->_rate;
436 0 0         return 'finished' if $self->{done} >= $self->{total};
437              
438 0 0 0       return 'unknown' if !defined $rate or $rate <= 0;
439              
440 0           my $duration = ($self->{total} - $self->{done}) / $rate;
441 0           return _human_readable_duration($duration);
442             }
443              
444             # Return rate for current progress
445             sub _rate {
446 0     0     my $self = shift;
447 0 0         return if !defined $self->{total};
448              
449 0           my $s = $self->{_samples};
450 0 0         return if @$s < 2;
451              
452             # Work out the last 5 rates and average them
453 0           my ($sum, $count) = (0,0);
454 0           for my $i (0..4) {
455 0 0         last if $i+1 > $#{$s};
  0            
456              
457             # Sample is a tuple of [count, time]
458 0           $sum += ($s->[$i][0] - $s->[$i+1][0]) / ($s->[$i][1] - $s->[$i+1][1]);
459 0           $count++;
460             }
461              
462 0           return $sum/$count;
463             }
464              
465             # Return current percentage complete, or undef if unknown
466             sub _percent {
467 0     0     my $self = shift;
468 0 0         return undef if !defined $self->{total};
469 0           my $pc = ($self->{done} / $self->{total}) * 100;
470 0 0         return $pc > 100 ? 100 : $pc;
471             }
472              
473             ## Bar styles
474              
475             sub _style_rainbow {
476 0     0     my ($percent, $size) = @_;
477              
478 0           my $rainbow = _ansi_rainbow();
479 0 0         if (!defined $percent) {
480             # Render a 100% width gray rainbow instead
481 0           $percent = 100;
482 0           $rainbow = _ansi_holding_pattern();
483             }
484              
485 0           my $fillsize = ($size * $percent / 100);
486              
487 0           my $bar = _unicode_block_bar($fillsize);
488 0           my $len = length $bar;
489 0           $bar = _color_bar($bar, $rainbow, 10);
490 0           $bar = encode('UTF-8', $bar);
491              
492 0           return $bar . (' ' x ($size - $len));
493             }
494              
495             sub _style_unicode {
496 0     0     my ($percent, $size) = @_;
497              
498 0           my $fillsize = ($size * $percent / 100);
499 0           my $bar = _unicode_block_bar($fillsize);
500 0           my $len = length $bar;
501 0           $bar = encode('UTF-8', $bar);
502              
503 0           return $bar . (' ' x ($size - $len));
504             }
505              
506             sub _color_bar {
507 0     0     my ($bar, $swatch, $speed) = @_;
508              
509 0           my $t = time * $speed;
510              
511             return join('', map {
512 0           colored(
  0            
513             substr($bar, $_ - 1, 1),
514             $swatch->[($_ + $t) % @$swatch],
515             )
516             } (1..length($bar)));
517             }
518              
519             sub _style_simple {
520 0     0     my ($percent, $size) = @_;
521 0           my $bar;
522 0 0         if (defined $percent) {
523 0           my $to_fill = int( $size * $percent / 100 );
524 0           $bar = ('#' x $to_fill) . (' ' x ($size - $to_fill));
525             }
526             else {
527 0           $bar = '-' x $size;
528             }
529 0           return $bar;
530             }
531              
532             sub _unicode_block_bar {
533 0     0     my $fillsize = shift;
534 0 0         return '' if $fillsize == 0;
535              
536 0           my $intpart = int($fillsize);
537 0           my $floatpart = $fillsize - $intpart;
538              
539 0           my $whole_block = chr(0x2588); # full block
540              
541             # Block range is U+2588 (full block) .. U+258F (left one eighth block)
542 0 0         my $last_block = $floatpart == 0 ? '' : chr(0x2588 + int((1 - $floatpart) * 8));
543              
544 0           return ($whole_block x $intpart) . $last_block;
545             }
546              
547             ## Utilities
548              
549             # Check format string to ensure it is valid
550             sub _check_format {
551 0     0     my ($param, $format) = @_;
552 0 0         defined $format or croak "format is undefined";
553              
554 0           for my $line (split /\n/, $format) {
555              
556 0           while ($line =~ /:(\w+)/g) {
557 0 0         exists $FORMAT_STRINGS{$1} or croak "$param: invalid format string ':$1'";
558             }
559              
560 0 0         if (($line =~ /:(?:bar|spacer)/) > 1) {
561 0           croak "$param: contains more than one bar or spacer, this isn't allowed :(";
562             }
563             }
564              
565 0           return $format;
566             }
567              
568             # Convert (positive) duration in seconds to a human-readable string
569             # e.g. '2 days', '14 hrs', '2 mins'
570             sub _human_readable_duration {
571 0     0     my $dur = shift;
572 0 0         return 'unknown' if !defined $dur;
573              
574 0 0         my ($val, $unit) = $dur < 60 ? ($dur, 'sec')
    0          
    0          
575             : $dur < 3600 ? ($dur / 60, 'min')
576             : $dur < 86400 ? ($dur / 3600, 'hr')
577             : ($dur / 86400, 'day')
578             ;
579 0 0         return int($val) . " $unit" . (int($val) == 1 ? '' : 's') . " left";
580             }
581              
582             # Convert rate (a number, assumed to be items/second) into a more
583             # appropriate form
584             sub _human_readable_item_rate {
585 0     0     my $rate = shift;
586 0 0         return 'unknown' if !defined $rate;
587              
588 0 0         my ($val, $unit) = $rate < 10**3 ? ($rate, '')
    0          
    0          
    0          
589             : $rate < 10**6 ? ($rate / 10**3, 'K')
590             : $rate < 10**9 ? ($rate / 10**6, 'M')
591             : $rate < 10**12 ? ($rate / 10**9, 'B')
592             : ($rate / 10**12, 'T')
593             ;
594 0           return sprintf('%.1f', $val) . "$unit/s";
595             }
596              
597             # Convert rate (a number, assumed to be bytes/second) into a more
598             # appropriate human-readable unit
599             # e.g. '3 KB/s', '14 MB/s'
600             sub _human_readable_byte_rate {
601 0     0     my $rate = shift;
602 0 0         return 'unknown' if !defined $rate;
603              
604 0 0         my ($val, $unit) = $rate < 1024 ? ($rate, 'byte')
    0          
    0          
    0          
605             : $rate < 1024**2 ? ($rate / 1024, 'KB')
606             : $rate < 1024**3 ? ($rate / 1024**2, 'MB')
607             : $rate < 1024**4 ? ($rate / 1024**3, 'GB')
608             : ($rate / 1024**4, 'TB')
609             ;
610 0           return int($val) . " $unit/s";
611             }
612              
613             sub _term_is_256color {
614 0     0     return $ENV{TERM} eq 'xterm-256color';
615             }
616              
617             sub _ansi_rainbow {
618 0 0   0     if (_term_is_256color()) {
619 0           return [map { "ansi$_" } (92, 93, 57, 21, 27, 33, 39, 45, 51, 50, 49, 48, 47, 46, 82, 118, 154, 190, 226, 220, 214, 208, 202, 196)];
  0            
620             }
621             else {
622 0           return [qw(magenta blue cyan green yellow red)];
623             }
624              
625             }
626              
627             sub _ansi_holding_pattern {
628 0 0   0     if (_term_is_256color()) {
629 0           return [map { "grey$_" } (0..23), reverse(1..22)];
  0            
630             }
631             else {
632             # Use a dotted pattern. XXX Maybe should be related to rate?
633             # XXX in genral animating by rate is good for finished bars too
634             # XXX rate does not drop to 0 when finished
635 0           return ['black', 'black', 'black', 'black', 'white'];
636             }
637             }
638              
639             ## Multiple bar support
640              
641             sub _register_bar {
642 0     0     my $bar = shift;
643 0   0       my $data = $REGISTRY{$bar->{fh}} ||= {};
644 0   0       push @{ $data->{bars} ||= [] }, $bar;
  0            
645 0 0 0       if (!defined $data->{maxbars} or $data->{maxbars} < @{$data->{bars}}) {
  0            
646 0           $data->{maxbars} = @{$data->{bars}};
  0            
647             }
648             }
649              
650             sub _unregister_bar {
651 0     0     my $bar = shift;
652 0 0         my $data = $REGISTRY{$bar->{fh}} or return;
653              
654 0           @{$data->{bars}} = grep { refaddr $_ ne refaddr $bar } @{$data->{bars}};
  0            
  0            
  0            
655              
656             # Are we the last bar? Move the cursor to the bottom of the bars.
657 0 0 0       if (@{$data->{bars}} == 0 && -t $bar->{fh}) {
  0            
658 0           print {$bar->{fh}} "\033[" . $data->{maxbars} . "B";
  0            
659             }
660             }
661              
662             sub _bars_for {
663 0     0     my $fh = shift;
664 0 0         return if !defined $fh;
665 0 0         return if !exists $REGISTRY{$fh};
666 0 0         return @{ $REGISTRY{$fh}{bars} || [] };
  0            
667             }
668              
669             1;
670              
671             =pod
672              
673             =head1 HEAD
674              
675             Progress::Awesome - an awesome progress bar that just works
676              
677             =head1 SYNOPSIS
678              
679             my $p = Progress::Awesome->new(100, style => 'rainbow');
680             for my $item (1..100) {
681             do_some_stuff();
682             $p->inc;
683             }
684              
685             # Multiline progress bars
686             my $p = Progress::Awesome(100, format => ":bar\n:left/:total :spacer ETA :eta");
687              
688             =head1 DESCRIPTION
689              
690             Similar to the venerable L with several enhancements:
691              
692             =over
693              
694             =item *
695              
696             Does the right thing when non-interactive - hides the progress bar and logs
697             intermittently with timestamps.
698              
699             =item *
700              
701             Completes itself when C is called or it goes out of scope, just in case
702             you forget.
703              
704             =item *
705              
706             Customisable format includes number of items, item processing rate, file transfer
707             rate (if items=bytes) and ETA. When non-interactive, logging format can also be
708             customised.
709              
710             =item *
711              
712             Gets out of your way - won't noisily complain if it can't work out the terminal
713             size, and won't die if you set the progress bar to its max value when it's already
714             reached the max (or for any other reason).
715              
716             =item *
717              
718             Can be incremented using C<++> or C<+=> if you like.
719              
720             =item *
721              
722             Works fine if max is undefined, set halfway through, or updated halfway through.
723              
724             =item *
725              
726             Estimates ETA with more intelligent prediction than simple linear.
727              
728             =item *
729              
730             Colours!!
731              
732             =item *
733              
734             Multiple process bars at once 'just work'.
735              
736             =back
737              
738             =head1 METHODS
739              
740             =over
741              
742             =item new ( %args )
743              
744             =item new ( total, %args )
745              
746             Create a new progress bar, passing arguments as a hash or hashref. If the first
747             argument looks like a number then it will be used as the bar's total number of
748             items.
749              
750             =over
751              
752             =item total (optional)
753              
754             Total number of items to be processed. (items, bytes, files, etc.)
755              
756             =item format (default: '[:bar] :done/:total :eta :rate')
757              
758             Specify a format for the progress bar (see L below).
759             C<:bar> or C<:spacer> parts will fill to all available space.
760              
761             =item style (optional)
762              
763             Specify the bar style. This may be a string ('rainbow' or 'boring') or a function
764             that accepts the percentage and size of the bar (in chars) and returns ANSI data
765             for the bar.
766              
767             =item title (optional)
768              
769             Optional bar title.
770              
771             =item log_format (default: '[:ts] :percent% :done/:total :eta :rate')
772              
773             Specify a format for log output used when the script is run non-interactively.
774              
775             =item log (default: 1)
776              
777             If set to 0, don't log anything when run non-interactively.
778              
779             =item color (default: 1)
780              
781             If set to 0, suppress colors when rendering the progress bar.
782              
783             =item remove (default: 0)
784              
785             If set to 1, remove the progress bar after completion via C.
786              
787             =item fh (default: \*STDERR)
788              
789             The filehandle to output to.
790              
791             =item done (default: 0)
792              
793             Starting number of items done.
794              
795             =back
796              
797             =item update ( value )
798              
799             Update the progress bar to the specified value. If undefined, the progress bar will go into
800             a spinning/unknown state.
801              
802             =item inc ( [value] )
803              
804             Increment progress bar by this many items, or 1 if omitted.
805              
806             =item finish
807              
808             Set the progress bar to maximum. Any further updates will not take effect. Happens automatically
809             when the progress bar goes out of scope.
810              
811             =item total ( [value] )
812              
813             Updates the total number of items. May be set to undef if unknown. With zero arguments,
814             returns the current total.
815              
816             =item dec ( [value] )
817              
818             Decrement the progress bar by this many items, or 1 if omitted.
819              
820             =back
821              
822             =head1 FORMATS
823              
824             A convenient way to specify what fields go where in your progress bar or log output.
825              
826             Format strings may span multiple lines, and may contain any of the below fields:
827              
828             =over
829              
830             =item :bar
831              
832             The progress bar. Expands to fill all available space not used by other fields.
833              
834             =item :spacer
835              
836             Expands to fill all available space. Items before the spacer will be aligned to
837             the left of the screen, and items after the spacer will be aligned to the right.
838             In this respect it's like a progress bar that's invisible.
839              
840             =item ::
841              
842             Literal ':'
843              
844             =item :ts
845              
846             Current timestamp (month, day, time) - intended for logging mode.
847              
848             =item :done
849              
850             Number of items that have been completed.
851              
852             =item :left
853              
854             Number of items remaining
855              
856             =item :total
857              
858             Maximum number of items.
859              
860             =item :eta
861              
862             Estimated time until progress bar completes.
863              
864             =item :rate
865              
866             Number of items being processed per second.
867              
868             =item :bytes
869              
870             Number of bytes being processed per second (expressed as KB, MB, GB etc. as needed)
871              
872             =item :percent
873              
874             Current percent completion (without % sign)
875              
876             =item :title
877              
878             Bar title. Title will be prepended automatically if not included in the
879             format string.
880              
881             =back
882              
883             =head1 REPORTING BUGS
884              
885             It's early days for this module so bugs are possible and feature requests are warmly
886             welcomed. We use L
887             for reports.
888              
889             =head1 AUTHOR
890              
891             Richard Harris richardjharris@gmail.com
892              
893             =head1 COPYRIGHT
894              
895             Copyright (c) 2017 Richard Harris. This program is free software; you can
896             redistribute it and/or modify it under the same terms as Perl itself.
897              
898             =cut
899              
900             __END__