File Coverage

blib/lib/Term/ProgressSpinner.pm
Criterion Covered Total %
statement 216 371 58.2
branch 61 164 37.2
condition 20 41 48.7
subroutine 40 56 71.4
pod 43 45 95.5
total 380 677 56.1


line stmt bran cond sub pod time code
1             package Term::ProgressSpinner;
2             our $VERSION = '1.02';
3 4     4   563100 use 5.006; use strict; use warnings;
  4     4   18  
  4     4   34  
  4         9  
  4         183  
  4         24  
  4         7  
  4         271  
4 4     4   3058 use IO::Handle; use Term::ANSIColor; use Time::HiRes qw//;
  4     4   50300  
  4     4   287  
  4         7483  
  4         76641  
  4         510  
  4         40  
  4         8  
  4         132  
5 4     4   6075 use utf8;
  4         1136  
  4         30  
6 4     4   2360 use Term::Size::Any qw/chars/;
  4         1419  
  4         34  
7             if ($^O eq 'MSWin32') { eval "use Win32::Console::ANSI; use Win32::Console;"; }
8             our (%SPINNERS, %PROGRESS, %VALIDATE);
9              
10             BEGIN {
11             %VALIDATE = (
12             colours => {
13             map {
14 4     4   20271 my $c = $_;
  32         65  
15             (
16             $c => 1,
17             "bright_${c}" => 1,
18 32         3119 (map { (
19 256         2087 "${c} on_${_}" => 1,
20             "${c} on_bright_${_}" => 1,
21             "bright_${c} on_${_}" => 1,
22             "bright_${c} on_bright_${_}" => 1,
23             ) } qw/black red green yellow blue magenta cyan white/)
24             )
25             } qw/black red green yellow blue magenta cyan white/
26             },
27             msg_regex => qr/\{(total|progress|spinner|percents|percentage|percentages|percent|counter|elapsed|elapsed_second|estimate|estimate_second|start_epoch|start_epoch_second|epoch|epoch_second|per_second|last_advance_epoch|last_advance_epoch_second|last_elapsed|last_elapsed_second|elapsed|elapsed_second)\}/
28             );
29 4         533 %SPINNERS = (
30             bar => {
31             width => 3,
32             index => [4, 2, 6],
33             chars => [
34             "▁",
35             "▂",
36             "▃",
37             "▄",
38             "▅",
39             "▆",
40             "▇",
41             "█"
42             ]
43             },
44             dots => {
45             width => 1,
46             index => [1],
47             chars => [
48             "⠋",
49             "⠙",
50             "⠹",
51             "⠸",
52             "⠼",
53             "⠴",
54             "⠦",
55             "⠧",
56             "⠇",
57             "⠏"
58             ]
59             },
60             around => {
61             width => 1,
62             index => [1],
63             chars => [
64             "⢀⠀",
65             "⡀⠀",
66             "⠄⠀",
67             "⢂⠀",
68             "⡂⠀",
69             "⠅⠀",
70             "⢃⠀",
71             "⡃⠀",
72             "⠍⠀",
73             "⢋⠀",
74             "⡋⠀",
75             "⠍⠁",
76             "⢋⠁",
77             "⡋⠁",
78             "⠍⠉",
79             "⠋⠉",
80             "⠋⠉",
81             "⠉⠙",
82             "⠉⠙",
83             "⠉⠩",
84             "⠈⢙",
85             "⠈⡙",
86             "⢈⠩",
87             "⡀⢙",
88             "⠄⡙",
89             "⢂⠩",
90             "⡂⢘",
91             "⠅⡘",
92             "⢃⠨",
93             "⡃⢐",
94             "⠍⡐",
95             "⢋⠠",
96             "⡋⢀",
97             "⠍⡁",
98             "⢋⠁",
99             "⡋⠁",
100             "⠍⠉",
101             "⠋⠉",
102             "⠋⠉",
103             "⠉⠙",
104             "⠉⠙",
105             "⠉⠩",
106             "⠈⢙",
107             "⠈⡙",
108             "⠈⠩",
109             "⠀⢙",
110             "⠀⡙",
111             "⠀⠩",
112             "⠀⢘",
113             "⠀⡘",
114             "⠀⠨",
115             "⠀⢐",
116             "⠀⡐",
117             "⠀⠠",
118             "⠀⢀",
119             "⠀⡀"
120             ]
121             },
122             pipe => {
123             width => 1,
124             index => [1],
125             chars => [
126             "┤",
127             "┘",
128             "┴",
129             "└",
130             "├",
131             "┌",
132             "┬",
133             "┐"
134             ]
135             },
136             moon => {
137             width => 1,
138             index => [1],
139             chars => [
140             "🌑 ",
141             "🌒 ",
142             "🌓 ",
143             "🌔 ",
144             "🌕 ",
145             "🌖 ",
146             "🌗 ",
147             "🌘 "
148             ]
149             },
150             circle => {
151             width => 1,
152             index => [1],
153             chars => [
154             "・",
155             "◦",
156             "●",
157             "○",
158             "◎",
159             "◉",
160             "⦿",
161             "◉",
162             "◎",
163             "○",
164             "◦",
165             "・",
166             ]
167             },
168             color_circle => {
169             width => 1,
170             index => [1],
171             chars => [
172             "🔴",
173             "🟠",
174             "🟡",
175             "🟢",
176             "🔵",
177             "🟣",
178             "⚫️",
179             "⚪️",
180             "🟤"
181             ]
182             },
183             color_circles => {
184             width => 3,
185             index => [1, 4, 7],
186             chars => [
187             "🔴",
188             "🟠",
189             "🟡",
190             "🟢",
191             "🔵",
192             "🟣",
193             "⚫️",
194             "⚪️",
195             "🟤"
196             ]
197             },
198             color_square => {
199             width => 1,
200             index => [1],
201             chars => [
202             "🟥",
203             "🟧",
204             "🟨",
205             "🟩",
206             "🟦",
207             "🟪",
208             "⬛️",
209             "⬜️",
210             "🟫"
211             ]
212             },
213             color_squares => {
214             width => 3,
215             index => [1, 3, 6],
216             chars => [
217             "🟥",
218             "🟧",
219             "🟨",
220             "🟩",
221             "🟦",
222             "🟪",
223             "⬛️",
224             "⬜️",
225             "🟫"
226             ]
227             },
228             earth => {
229             width => 1,
230             index => [1],
231             chars => [
232             "🌎",
233             "🌍",
234             "🌏"
235             ]
236             },
237             circle_half => {
238             width => 1,
239             index => [1],
240             chars => [
241             '◐',
242             '◓',
243             '◑',
244             '◒'
245             ]
246             },
247             clock => {
248             width => 1,
249             index => [1],
250             chars => [
251             "🕛 ",
252             "🕐 ",
253             "🕑 ",
254             "🕒 ",
255             "🕓 ",
256             "🕔 ",
257             "🕕 ",
258             "🕖 ",
259             "🕗 ",
260             "🕘 ",
261             "🕙 ",
262             "🕚 "
263             ]
264             },
265             pong => {
266             width => 1,
267             index => [1],
268             chars => [
269             "▐⠂ ▌",
270             "▐⠈ ▌",
271             "▐ ⠂ ▌",
272             "▐ ⠠ ▌",
273             "▐ ⡀ ▌",
274             "▐ ⠠ ▌",
275             "▐ ⠂ ▌",
276             "▐ ⠈ ▌",
277             "▐ ⠂ ▌",
278             "▐ ⠠ ▌",
279             "▐ ⡀ ▌",
280             "▐ ⠠ ▌",
281             "▐ ⠂ ▌",
282             "▐ ⠈ ▌",
283             "▐ ⠂▌",
284             "▐ ⠠▌",
285             "▐ ⡀▌",
286             "▐ ⠠ ▌",
287             "▐ ⠂ ▌",
288             "▐ ⠈ ▌",
289             "▐ ⠂ ▌",
290             "▐ ⠠ ▌",
291             "▐ ⡀ ▌",
292             "▐ ⠠ ▌",
293             "▐ ⠂ ▌",
294             "▐ ⠈ ▌",
295             "▐ ⠂ ▌",
296             "▐ ⠠ ▌",
297             "▐ ⡀ ▌",
298             "▐⠠ ▌"
299             ]
300             },
301             material => {
302             width => 1,
303             index => [1],
304             chars => [
305             "█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
306             "██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
307             "███▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
308             "████▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
309             "██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
310             "██████▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
311             "███████▁▁▁▁▁▁▁▁▁▁▁▁▁",
312             "████████▁▁▁▁▁▁▁▁▁▁▁▁",
313             "█████████▁▁▁▁▁▁▁▁▁▁▁",
314             "█████████▁▁▁▁▁▁▁▁▁▁▁",
315             "██████████▁▁▁▁▁▁▁▁▁▁",
316             "███████████▁▁▁▁▁▁▁▁▁",
317             "█████████████▁▁▁▁▁▁▁",
318             "██████████████▁▁▁▁▁▁",
319             "██████████████▁▁▁▁▁▁",
320             "▁██████████████▁▁▁▁▁",
321             "▁██████████████▁▁▁▁▁",
322             "▁██████████████▁▁▁▁▁",
323             "▁▁██████████████▁▁▁▁",
324             "▁▁▁██████████████▁▁▁",
325             "▁▁▁▁█████████████▁▁▁",
326             "▁▁▁▁██████████████▁▁",
327             "▁▁▁▁██████████████▁▁",
328             "▁▁▁▁▁██████████████▁",
329             "▁▁▁▁▁██████████████▁",
330             "▁▁▁▁▁██████████████▁",
331             "▁▁▁▁▁▁██████████████",
332             "▁▁▁▁▁▁██████████████",
333             "▁▁▁▁▁▁▁█████████████",
334             "▁▁▁▁▁▁▁█████████████",
335             "▁▁▁▁▁▁▁▁████████████",
336             "▁▁▁▁▁▁▁▁████████████",
337             "▁▁▁▁▁▁▁▁▁███████████",
338             "▁▁▁▁▁▁▁▁▁███████████",
339             "▁▁▁▁▁▁▁▁▁▁██████████",
340             "▁▁▁▁▁▁▁▁▁▁██████████",
341             "▁▁▁▁▁▁▁▁▁▁▁▁████████",
342             "▁▁▁▁▁▁▁▁▁▁▁▁▁███████",
343             "▁▁▁▁▁▁▁▁▁▁▁▁▁▁██████",
344             "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████",
345             "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████",
346             "█▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████",
347             "██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
348             "██▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
349             "███▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
350             "████▁▁▁▁▁▁▁▁▁▁▁▁▁▁██",
351             "█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
352             "█████▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
353             "██████▁▁▁▁▁▁▁▁▁▁▁▁▁█",
354             "████████▁▁▁▁▁▁▁▁▁▁▁▁",
355             "█████████▁▁▁▁▁▁▁▁▁▁▁",
356             "█████████▁▁▁▁▁▁▁▁▁▁▁",
357             "█████████▁▁▁▁▁▁▁▁▁▁▁",
358             "█████████▁▁▁▁▁▁▁▁▁▁▁",
359             "███████████▁▁▁▁▁▁▁▁▁",
360             "████████████▁▁▁▁▁▁▁▁",
361             "████████████▁▁▁▁▁▁▁▁",
362             "██████████████▁▁▁▁▁▁",
363             "██████████████▁▁▁▁▁▁",
364             "▁██████████████▁▁▁▁▁",
365             "▁██████████████▁▁▁▁▁",
366             "▁▁▁█████████████▁▁▁▁",
367             "▁▁▁▁▁████████████▁▁▁",
368             "▁▁▁▁▁████████████▁▁▁",
369             "▁▁▁▁▁▁███████████▁▁▁",
370             "▁▁▁▁▁▁▁▁█████████▁▁▁",
371             "▁▁▁▁▁▁▁▁█████████▁▁▁",
372             "▁▁▁▁▁▁▁▁▁█████████▁▁",
373             "▁▁▁▁▁▁▁▁▁█████████▁▁",
374             "▁▁▁▁▁▁▁▁▁▁█████████▁",
375             "▁▁▁▁▁▁▁▁▁▁▁████████▁",
376             "▁▁▁▁▁▁▁▁▁▁▁████████▁",
377             "▁▁▁▁▁▁▁▁▁▁▁▁███████▁",
378             "▁▁▁▁▁▁▁▁▁▁▁▁███████▁",
379             "▁▁▁▁▁▁▁▁▁▁▁▁▁███████",
380             "▁▁▁▁▁▁▁▁▁▁▁▁▁███████",
381             "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█████",
382             "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████",
383             "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████",
384             "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁████",
385             "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
386             "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁███",
387             "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██",
388             "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██",
389             "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁██",
390             "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
391             "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
392             "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁█",
393             "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
394             "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
395             "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁",
396             "▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁"
397              
398             ]
399              
400             }
401             );
402 4         26 $SPINNERS{default} = $SPINNERS{bar};
403 4         96 %PROGRESS = (
404             bar => {
405             chars => ['│', "█", '│']
406             },
407             equal => {
408             chars => ['[', "=", ']']
409             },
410             arrow => {
411             chars => ['│', "→", '│']
412             },
413             boxed_arrow => {
414             chars => ['│', "⍈", '│']
415             },
416             lines => {
417             chars => ['│', "≡", '│']
418             },
419             horizontal_lines => {
420             chars => ['│', "▤", '│']
421             },
422             vertical_lines => {
423             chars => ['│', "▥", '│']
424             },
425             hash => {
426             chars => ['[', "#", ']']
427             },
428             triangle => {
429             chars => ['│', '▶︎', '│' ]
430             },
431             den_triangle => {
432             chars => ['│', '⏅', '│' ]
433             },
434             circle => {
435             chars => ['│', 'Ⓞ', '│' ]
436             },
437             den_circle => {
438             chars => ['│', '⏂', '│' ]
439             },
440             shekel => {
441             chars => ['│', '₪', '│' ]
442             },
443             dots => {
444             chars => ['│', '▒', '│' ]
445             },
446             square => {
447             chars => ['│', '■', '│' ]
448             },
449             block => {
450             chars => ["【", "=", "】"]
451             }
452             );
453 4         29765 $PROGRESS{default} = $PROGRESS{bar};
454             }
455              
456             sub new {
457 25 50   25 1 411048 my ($pkg, %args) = (shift, ref $_[0] ? %{$_[0]} : @_);
  0         0  
458             $args{$_} and ($VALIDATE{colours}{$args{$_}} or die "Invalid color for $_")
459 25   50     783 for qw/text_color total_color counter_color percent_color percentage_color percentages_color percents_color spinner_color progress_color elapsed_color last_elapsed_color estimate_color last_advance_epoch_color start_epoch_color epoch_color/;
      66        
460 25   100     151 $args{precision} ||= 3;
461             # Set UTF-8 encoding on output handle for Unicode spinner characters
462 25   100     120 my $output = $args{output} // \*STDERR;
463 25 50 33 2   725 binmode($output, ':encoding(UTF-8)') unless ref($output) eq 'GLOB' && tied(*$output);
  2         1411  
  2         32  
  2         14  
464             return bless {
465             text_color => 'white',
466             total_color => 'white',
467             counter_color => 'white',
468             percent_color => 'white',
469             percentage_color => 'white',
470             percentages_color => 'white',
471             percents_color => 'white',
472             spinner_color => 'white',
473             elapsed_color => 'white',
474             start_epoch_color => 'white',
475             last_elapsed_color => 'white',
476             last_advance_epoch_color => 'white',
477             estimate_color => 'white',
478             epoch_color => 'white',
479             per_second_color => 'white',
480             spinner_options => $SPINNERS{ $args{spinner} || 'default' },
481             progress_color => 'white',
482             progress_width => 20,
483 25   50     5182 progress_options => $PROGRESS{ $args{progress} || 'default' },
      50        
      66        
484             output => $output,
485             progress_spinner_index => 0,
486             progress_spinners => [],
487             message => "{progress} {spinner} processed {percents} of {counter}/{total} {elapsed}/{estimate}",
488             terminal_height => 0,
489             terminal_line => 0,
490             %args
491             }, ref $pkg || $pkg;
492             }
493              
494             sub progress_spinner_index {
495 356     356 1 620 my ($self, $val) = @_;
496 356 100       3091 if (defined $val) {
497 23 50 33     177 if (ref $val || $val !~ m/\d+/) {
498 0         0 die 'progress_spinner_index should be a integer';
499             }
500 23         43 $self->{progress_spinner_index} = $val;
501             }
502 356         1045 return $self->{progress_spinner_index};
503             }
504              
505             sub terminal_height {
506 46     46 1 90 my ($self, $val) = @_;
507 46 50       123 if (defined $val) {
508 0 0 0     0 if (ref $val || $val !~ m/\d+/) {
509 0         0 die 'terminal_height should be a integer';
510             }
511 0         0 $self->{terminal_height} = $val;
512             }
513 46         175 return $self->{terminal_height};
514             }
515              
516             sub terminal_line {
517 69     69 1 277 my ($self, $val) = @_;
518 69 100       159 if (defined $val) {
519 23 50 33     262 if (ref $val || $val !~ m/\d+/) {
520 0         0 die 'terminal_line should be a integer';
521             }
522 23         63 $self->{terminal_line} = $val;
523             }
524 69         219 return $self->{terminal_line};
525             }
526              
527             sub progress_spinners {
528 1190     1190 1 2855 my ($self, $val) = @_;
529 1190 50       3723 if (defined $val) {
530 0 0 0     0 if (ref $val || "" ne 'ARRAY') {
531 0         0 die 'progress_spinners should be a array';
532             }
533 0         0 $self->{progress_spinners} = $val;
534             }
535 1190         5046 return $self->{progress_spinners};
536             }
537              
538             sub savepos {
539 23     23 1 39 my $self = shift;
540 23 50       93 my ($col, $rows) = $self->terminal_height ? (0, $self->terminal_height) : (Term::Size::Any::chars($self->output));
541 23         50 my $x = '';
542 23 50       51 if ($self->terminal_line) {
    0          
543 23         92 $x = $self->terminal_line;
544             } elsif ($^O eq 'MSWin32') {
545 0         0 my $CONSOLE = Win32::Console->new(Win32::Console::STD_OUTPUT_HANDLE());
546 0         0 ($x) = $CONSOLE->Cursor();
547             } else {
548 0         0 system "stty cbreak /dev/tty 2>&1";
549 0         0 $self->output->print("\e[6n");
550 0         0 $x .= getc STDIN for 0 .. 5;
551 0         0 system "stty -cbreak /dev/tty 2>&1";
552 0         0 my($n, $m)=$x=~m/(\d+)\;(\d+)/;
553 0         0 $x = $n;
554 0         0 $self->clear();
555             }
556 23 50       65 if ($x == $rows) {
557 0         0 $x--;
558 0         0 for (@{ $self->progress_spinners }) {
  0         0  
559 0         0 $_->{savepos} = $_->{savepos} - 1;
560             }
561             }
562 23         55 $self->{savepos} = $x;
563             }
564              
565             sub loadpos {
566 724     724 1 1842 my $self = shift;
567 724         2761 my $pos = $self->{savepos};
568 724         5268 $self->output->print("\e[$pos;1f");
569             }
570              
571             sub start {
572 23     23 1 78 my ($self, $total) = @_;
573 23 50       106 $self->total($total) if $total;
574 23         269 $self->start_epoch(Time::HiRes::time);
575 23         62 $self->output->print("\e[?25l");
576 23         796 $self->savepos;
577 23         54 $self->output->print("\n");
578 23         657 my $ps = $self->new(%{$self});
  23         290  
579 23         174 push @{ $self->progress_spinners }, $ps;
  23         77  
580 23         65 $self->progress_spinner_index($self->progress_spinner_index + 1);
581 23         131 return $ps;
582             }
583            
584             sub advance {
585 1001     1001 1 4662 my ($self, $ps, $prevent) = @_;
586 1001 100       3106 if ($ps) {
587 633 100       7185 if ($ps->counter < $ps->total) {
588 520         1667 $ps->counter($ps->counter + 1);
589 520         1854 my $spinner = $ps->spinner;
590 520         2199 for (1 .. $spinner->{width}) {
591 1080         3530 my $index = $spinner->{index}->[$_ - 1];
592 1080         2221 $spinner->{index}->[$_ - 1] = ($index + 1) % scalar @{$spinner->{chars}};
  1080         3868  
593             }
594 520 50       2308 select(undef, undef, undef, $ps->slowed) if $ps->slowed;
595 520 100       29390 $ps->draw() unless $prevent;
596             } else {
597 113         589 $self->finish($ps);
598             }
599             } else {
600 368         732 for my $spinner (@{$self->progress_spinners}) {
  368         1150  
601 429         2286 $self->advance($spinner, 1);
602             }
603 368 100       1134 scalar @{$self->{progress_spinners}} ? $self->draw() : $self->finish;
  368         5334  
604             }
605             }
606              
607             sub time_advance_elapsed {
608 724     724 0 2059 my ($self) = @_;
609 724         2263 my %time = ();
610 724         4418 $time{epoch} = sprintf($self->precision, Time::HiRes::time);
611 724         2213 $time{start_epoch} = sprintf($self->precision, $self->start_epoch);
612 724   66     10344 $time{last_advance_epoch} = $self->last_advance_epoch || $time{start_epoch};
613 724         2103 $time{last_elapsed} = sprintf($self->precision, $time{epoch} - $time{last_advance_epoch}) + 0;
614 724         2247 $time{elapsed} = sprintf($self->precision, $time{epoch} - $time{start_epoch}) + 0;
615 724         2862 for (qw/epoch start_epoch last_advance_epoch last_elapsed elapsed/) {
616 3620         18665 $time{"${_}_second"} = int($time{$_});
617             }
618 724         3779 $self->last_advance_epoch($time{epoch});
619 724         9690 return %time;
620             }
621              
622             sub draw {
623 1186     1186 1 3974 my ($self, $ps) = @_;
624 1186 100       3678 if ($ps) {
625 724         3913 $ps->loadpos;
626 724         127172 $ps->clear();
627 724         3572 my ($spinner, $progress, $available, %options) = ($ps->spinner, $ps->progress, $ps->progress_width, $ps->time_advance_elapsed);
628 724         3647 $options{total} = $ps->total;
629 724         2653 $options{counter} = $ps->counter;
630 724         2915 $options{spinner} = color($ps->spinner_color);
631             $options{spinner} .= $spinner->{chars}->[
632             $spinner->{index}->[$_ - 1]
633 724         46378 ] for (1 .. $spinner->{width});
634 724         2877 $options{spinner} .= color($ps->text_color);
635 724         33446 $options{percent} = int( ( $options{counter} / $options{total} ) * 100 );
636 724         3585 $options{percentage} = ($available / 100) * $options{percent};
637 724 100       3202 $options{estimate} = $options{percent} ? sprintf($self->precision, (($options{elapsed} / $options{percent}) * 100) - $options{elapsed}) + 0 : 0;
638 724         3652 $options{estimate_second} = int($options{estimate} + 0.5);
639             $options{per_second} = $options{elapsed_seconds} ?
640 724 50       2731 int(($options{counter} / int($options{elapsed_second})) + 0.5)
641             : 0;
642             $options{progress} = sprintf("%s%s%s%s%s",
643             color($ps->progress_color),
644             $progress->{chars}->[0],
645             ( $progress->{chars}->[1] x int($options{percentage} + 0.5) ) . ( ' ' x int( ($available - $options{percentage}) + 0.5 ) ),
646 724         2420 $progress->{chars}->[2],
647             color($ps->text_color)
648             );
649 724         30536 $options{percentages} = $options{percentage} . '%';
650 724         2668 $options{percents} = $options{percent} . '%';
651             $options{$_} = sprintf ("%s%s%s",
652             color($ps->{$_ . "_color"}),
653             $options{$_},
654             color($ps->text_color)
655 724         4140 ) for (qw/total percent percents percentage counter per_second/);
656 724         23774 for (qw/elapsed last_elapsed estimate last_advance_epoch start_epoch epoch/) {
657             $options{$_} = sprintf ("%s%s%s",
658             color($ps->{$_ . "_color"}),
659 4344         124814 $options{$_},
660             color($ps->text_color)
661             );
662             $options{"${_}_second"} = sprintf ("%s%s%s",
663             color($ps->{$_ . "_color"}),
664 4344         141878 $options{"${_}_second"},
665             color($ps->text_color)
666             );
667             }
668 724         23977 my $message = $ps->message;
669 724         33002 $message =~ s/$VALIDATE{msg_regex}/$options{$1}/ig;
670 724         3074 $message .= color('reset') . "\n";
671 724         28444 $ps->output->print($message);
672 724         184697 return $ps->drawn(1);
673             } else {
674 462         1249 for my $spinner (@{$self->progress_spinners}) {
  462         2936  
675 724         2663 $self->draw($spinner);
676             }
677 462         2769 return $self->drawn(1);
678             }
679             }
680            
681             sub finish {
682 129     129 1 263 my ($self, $sp) = @_;
683 129 100 100     553 if ($sp && scalar @{$self->progress_spinners}) {
  113         291  
684 112         176 my $i = 0;
685 112         174 for (@{ $self->progress_spinners }) {
  112         251  
686 155 100       502 if ($sp->progress_spinner_index == $_->progress_spinner_index) {
687 23         59 last;
688             }
689 132         270 $i++;
690             }
691 112         259 splice @{$self->progress_spinners}, $i, 1;
  112         274  
692              
693             } else {
694 17         53 $self->output->print("\e[?25h");
695 17         1123 $self->finished(1);
696             }
697 129         515 return 0;
698             }
699              
700             sub finished {
701 273 100   273 0 704 if (defined $_[1]) {
702 17         45 $_[0]->{finished} = $_[1];
703             }
704 273         746 $_[0]->{finished};
705             }
706              
707             sub drawn {
708 1925     1925 1 4970 my ($self, $val) = @_;
709 1925 50       5800 if (defined $val) {
710 1925         5130 $self->{drawn} = $val;
711             }
712 1925         12970 return $self->{drawn};
713             }
714            
715             sub clear {
716 739     739 1 5248 my ($self) = @_;
717 739         2522 $self->output->print("\r\e[2K");
718 739         50708 $self->drawn(0);
719             }
720              
721             sub message {
722 724     724 1 4090 my ($self, $val) = @_;
723 724 50       3443 if (defined $val) {
724 0 0       0 if (ref $val) {
725 0         0 die 'message should be a string';
726             }
727 0         0 $self->{message} = $val;
728             }
729 724         7923 return $self->{message};
730             }
731              
732             sub output {
733 2253     2253 1 6704 my ($self, $val) = @_;
734 2253 50       9759 if (defined $val) {
735 0         0 $self->{output} = $val;
736             }
737 2253         17562 return $self->{output};
738             }
739              
740             sub total {
741 1380     1380 1 3311 my ($self, $val) = @_;
742 1380 100       3706 if (defined $val) {
743 23 50       112 if ($val !~ m/\d+/) {
744 0         0 die "total should be a integer";
745             }
746 23         58 $self->{total} = $val;
747 23         52 $self->{counter} = 0;
748             }
749 1380         5382 return $self->{total};
750             }
751              
752             sub slowed {
753 1042     1042 1 3089 my ($self, $val) = @_;
754 1042 100       4534 if (defined $val) {
755 2 50       42 if ($val !~ m/\d+(\.\d+)?/) {
756 0         0 die "slowed should be a float";
757             }
758 2         8 $self->{slowed} = $val;
759             }
760 1042         52453590 return $self->{slowed};
761             }
762              
763             sub counter {
764 2397     2397 1 5118 my ($self, $val) = @_;
765 2397 100       7728 if (defined $val) {
766 520 50       3977 if ($val !~ m/\d+/) {
767 0         0 die "counter should be a integer";
768             }
769 520         1388 $self->{counter} = $val;
770             }
771 2397         7762 return $self->{counter};
772             }
773              
774             sub start_epoch {
775 747     747 1 2158 my ($self, $val) = @_;
776 747 100       4854 if (defined $val) {
777 23 50       285 if ($val !~ m/\d+(\.\d+)?/) {
778 0         0 die "start_epoch should be a epoch";
779             }
780 23         68 $self->{start_epoch} = $val;
781             }
782 747         5658 return $self->{start_epoch};
783             }
784              
785             sub last_advance_epoch {
786 1448     1448 1 3653 my ($self, $val) = @_;
787 1448 100       3975 if (defined $val) {
788 724 50       8988 if ($val !~ m/\d+(\.\d+)?/) {
789 0         0 die "last_advance_epoch should be a epoch";
790             }
791 724         1908 $self->{last_advance_epoch} = $val;
792             }
793 1448         4791 return $self->{last_advance_epoch};
794             }
795              
796             sub precision {
797 3614     3614 1 14084 my ($self, $val) = @_;
798 3614 50       8501 if (defined $val) {
799 0 0       0 if ($val !~ m/\d+/) {
800 0         0 die "last_advance_epoch should be a epoch";
801             }
802 0         0 $self->{precision} = $val;
803             }
804 3614         8841 $val = $self->{precision};
805 3614         43741 return "%.${val}f";
806             }
807              
808             sub text_color {
809 14480     14480 1 517430 my ($self, $val) = @_;
810 14480 50       45845 if (defined $val) {
811 0 0       0 unless ($VALIDATE{colours}{$val}) {
812 0         0 die "$val is not a valid color";
813             }
814 0         0 $self->{text_color} = $val;
815             }
816 14480         44684 return $self->{text_color};
817             }
818              
819             sub spinner {
820 1259     1259 1 3128 my ($self, $spinner) = @_;
821 1259 100 50     4635 $self->{spinner_options} = $SPINNERS{$spinner} or die "Invalid spinner $spinner" if $spinner;
822 1259         12175 $self->{spinner_options};
823             }
824              
825             sub spinner_color {
826 724     724 1 1933 my ($self, $val) = @_;
827 724 50       3469 if (defined $val) {
828 0 0       0 unless ($VALIDATE{colours}{$val}) {
829 0         0 die "$val is not a valid color";
830             }
831 0         0 $self->{spinner_color} = $val;
832             }
833 724         5068 return $self->{spinner_color};
834             }
835              
836             sub progress {
837 739     739 1 3954 my ($self, $progress) = @_;
838 739 100 50     2207 $self->{progress_options} = $PROGRESS{$progress} or die "Invalid progress $progress" if $progress;
839 739         3734 $self->{progress_options};
840             }
841              
842             sub progress_color {
843 724     724 1 1987 my ($self, $val) = @_;
844 724 50       1847 if (defined $val) {
845 0 0       0 unless ($VALIDATE{colours}{$val}) {
846 0         0 die "$val is not a valid color";
847             }
848 0         0 $self->{progress_color} = $val;
849             }
850 724         2617 return $self->{progress_color};
851             }
852              
853             sub progress_width {
854 724     724 1 1741 my ($self, $val) = @_;
855 724 50       3028 if (defined $val) {
856 0         0 $self->{progress_width} = $val;
857             }
858 724         3604 return $self->{progress_width};
859             }
860              
861             sub percent_color {
862 0     0 1 0 my ($self, $val) = @_;
863 0 0       0 if (defined $val) {
864 0 0       0 unless ($VALIDATE{colours}{$val}) {
865 0         0 die "$val is not a valid color";
866             }
867 0         0 $self->{percent_color} = $val;
868             }
869 0         0 return $self->{percent_color};
870             }
871              
872             sub percents_color {
873 0     0 1 0 my ($self, $val) = @_;
874 0 0       0 if (defined $val) {
875 0 0       0 unless ($VALIDATE{colours}{$val}) {
876 0         0 die "$val is not a valid color";
877             }
878 0         0 $self->{percents_color} = $val;
879             }
880 0         0 return $self->{percents_color};
881             }
882              
883             sub percentage_color {
884 0     0 1 0 my ($self, $val) = @_;
885 0 0       0 if (defined $val) {
886 0 0       0 unless ($VALIDATE{colours}{$val}) {
887 0         0 die "$val is not a valid color";
888             }
889 0         0 $self->{percentage_color} = $val;
890             }
891 0         0 return $self->{percentage_color};
892             }
893              
894             sub percentages_color {
895 0     0 1 0 my ($self, $val) = @_;
896 0 0       0 if (defined $val) {
897 0 0       0 unless ($VALIDATE{colours}{$val}) {
898 0         0 die "$val is not a valid color";
899             }
900 0         0 $self->{percentages_color} = $val;
901             }
902 0         0 return $self->{percentages_color};
903             }
904              
905             sub total_color {
906 0     0 1 0 my ($self, $val) = @_;
907 0 0       0 if (defined $val) {
908 0 0       0 unless ($VALIDATE{colours}{$val}) {
909 0         0 die "$val is not a valid color";
910             }
911 0         0 $self->{total_color} = $val;
912             }
913 0         0 return $self->{total_color};
914             }
915              
916             sub counter_color {
917 0     0 1 0 my ($self, $val) = @_;
918 0 0       0 if (defined $val) {
919 0 0       0 unless ($VALIDATE{colours}{$val}) {
920 0         0 die "$val is not a valid color";
921             }
922 0         0 $self->{counter_color} = $val;
923             }
924 0         0 return $self->{counter_color};
925             }
926              
927             sub elapsed_color {
928 0     0 1 0 my ($self, $val) = @_;
929 0 0       0 if (defined $val) {
930 0 0       0 unless ($VALIDATE{colours}{$val}) {
931 0         0 die "$val is not a valid color";
932             }
933 0         0 $self->{elapsed_color} = $val;
934             }
935 0         0 return $self->{elapsed_color};
936             }
937              
938             sub last_elapsed_color {
939 0     0 1 0 my ($self, $val) = @_;
940 0 0       0 if (defined $val) {
941 0 0       0 unless ($VALIDATE{colours}{$val}) {
942 0         0 die "$val is not a valid color";
943             }
944 0         0 $self->{last_elapsed_color} = $val;
945             }
946 0         0 return $self->{last_elapsed_color};
947             }
948              
949             sub estimate_color {
950 0     0 1 0 my ($self, $val) = @_;
951 0 0       0 if (defined $val) {
952 0 0       0 unless ($VALIDATE{colours}{$val}) {
953 0         0 die "$val is not a valid color";
954             }
955 0         0 $self->{estimate_elapsed_color} = $val;
956             }
957 0         0 return $self->{estimate_elapsed_color};
958             }
959              
960             sub last_advance_epoch_color {
961 0     0 1 0 my ($self, $val) = @_;
962 0 0       0 if (defined $val) {
963 0 0       0 unless ($VALIDATE{colours}{$val}) {
964 0         0 die "$val is not a valid color";
965             }
966 0         0 $self->{last_advance_epoch_color} = $val;
967             }
968 0         0 return $self->{last_advance_epoch_color};
969             }
970              
971             sub start_epoch_color {
972 0     0 1 0 my ($self, $val) = @_;
973 0 0       0 if (defined $val) {
974 0 0       0 unless ($VALIDATE{colours}{$val}) {
975 0         0 die "$val is not a valid color";
976             }
977 0         0 $self->{start_epoch_color} = $val;
978             }
979 0         0 return $self->{start_epoch_color};
980             }
981              
982             sub epoch_color {
983 0     0 1 0 my ($self, $val) = @_;
984 0 0       0 if (defined $val) {
985 0 0       0 unless ($VALIDATE{colours}{$val}) {
986 0         0 die "$val is not a valid color";
987             }
988 0         0 $self->{epoch_color} = $val;
989             }
990 0         0 return $self->{epoch_color};
991             }
992              
993             sub per_second_color {
994 0     0 1 0 my ($self, $val) = @_;
995 0 0       0 if (defined $val) {
996 0 0       0 unless ($VALIDATE{colours}{$val}) {
997 0         0 die "$val is not a valid color";
998             }
999 0         0 $self->{per_second_color} = $val;
1000             }
1001 0         0 return $self->{per_second_color};
1002             }
1003              
1004             sub sleep {
1005 16     16 1 1757448 select(undef, undef, undef, $_[1]);
1006             }
1007              
1008             sub start_async {
1009 0     0 1 0 my ($self, $loop, %opts) = @_;
1010 0   0     0 my $interval = $opts{interval} // 0.1;
1011              
1012             # Initialize for async mode - use a pseudo progress spinner
1013 0         0 my $ps = $self->start(1); # Total of 1, we won't actually advance counter
1014 0         0 $ps->counter(0); # Keep at 0 so we don't auto-finish
1015              
1016             # Create and store the timer
1017 0         0 require IO::Async::Timer::Periodic;
1018             my $timer = IO::Async::Timer::Periodic->new(
1019             interval => $interval,
1020             on_tick => sub {
1021 0 0   0   0 return if $self->finished;
1022             # Advance the spinner animation without incrementing counter
1023 0         0 my $spinner = $ps->spinner;
1024 0         0 for (1 .. $spinner->{width}) {
1025 0         0 my $index = $spinner->{index}->[$_ - 1];
1026 0         0 $spinner->{index}->[$_ - 1] = ($index + 1) % scalar @{$spinner->{chars}};
  0         0  
1027             }
1028 0         0 $self->draw($ps);
1029             },
1030 0         0 );
1031 0         0 $timer->start;
1032 0         0 $loop->add($timer);
1033 0         0 $self->{_async_timer} = $timer;
1034 0         0 $self->{_async_loop} = $loop;
1035 0         0 $self->{_async_ps} = $ps;
1036              
1037             # Initial draw
1038 0         0 $self->draw($ps);
1039              
1040 0         0 return $self;
1041             }
1042              
1043             sub stop_async {
1044 0     0 1 0 my ($self, $message) = @_;
1045              
1046 0 0       0 if ($self->{_async_timer}) {
1047 0         0 $self->{_async_timer}->stop;
1048 0 0       0 $self->{_async_loop}->remove($self->{_async_timer}) if $self->{_async_loop};
1049 0         0 delete $self->{_async_timer};
1050 0         0 delete $self->{_async_loop};
1051             }
1052              
1053             # Finish the progress spinner
1054 0 0       0 if ($self->{_async_ps}) {
1055 0         0 $self->finish($self->{_async_ps});
1056 0         0 delete $self->{_async_ps};
1057             }
1058 0         0 $self->finish();
1059              
1060             # Print completion message if provided
1061 0 0       0 if ($message) {
1062 0         0 $self->output->print(Term::ANSIColor::colored(['green'], "✓ ") . "$message\n");
1063             }
1064              
1065 0         0 return $self;
1066             }
1067              
1068             1;
1069              
1070             __END__;