File Coverage

blib/lib/IO/Term/Status.pm
Criterion Covered Total %
statement 47 54 87.0
branch 16 20 80.0
condition n/a
subroutine 11 12 91.6
pod 6 6 100.0
total 80 92 86.9


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Paul Evans, 2021 -- leonerd@leonerd.org.uk
5              
6             package IO::Term::Status 0.01;
7              
8 4     4   230898 use v5.14;
  4         46  
9 4     4   21 use warnings;
  4         8  
  4         195  
10 4     4   33 use base qw( IO::Handle );
  4         8  
  4         2534  
11              
12             my $EL = "\e[K";
13             my $CLEARLINE = "\r$EL";
14             my $PREVLINE = "\eM";
15             my $PREVLINEHOME = "\r$PREVLINE";
16              
17 4         8 use constant HAVE_STRING_TAGGED_TERMINAL => defined eval {
18 4         4483 require String::Tagged::Terminal;
19 4     4   27703 };
  4         9  
20              
21             =head1 NAME
22              
23             C - print log lines to a terminal with a running status bar
24              
25             =head1 SYNOPSIS
26              
27             use IO::Term::Status;
28              
29             my $io = IO::Term::Status->new_for_stdout;
30              
31             $io->set_status( "Running" );
32              
33             my @items = ...;
34              
35             foreach my $idx ( 0 .. $#items ) {
36             $io->set_status( sprintf "Running | %d of %d", $idx+1, scalar @items );
37              
38             my $item = $items[$idx];
39             $io->print_line( "Processing item $item..." );
40             ...
41             }
42              
43             $io->set_status( "" ); # Clear the status line before exiting
44              
45             =head1 DESCRIPTION
46              
47             This module provides a subclass of L for maintaining a running
48             status display on the terminal. It presumes the terminal can handle basic ANSI
49             control characters (thus is not suitable for printing to log files, etc).
50              
51             The "status bar" consists of a single additional line of text, printed below
52             the current log of output. More lines of regular log can be printed using the
53             L method, which maintains the running status bar below the
54             output.
55              
56             =head2 With C
57              
58             If the L module is available, then the status string
59             can set to an instance of L, obeying the
60             L tag conventions. This will be converted to
61             terminal output.
62              
63             As an extra convenience, whatever the prevailing background colour is at the
64             end of the string will be preserved for line-erase purposes, meaning that
65             colour will extend the entire width of the status bar line.
66              
67             =cut
68              
69             *is_string_tagged = HAVE_STRING_TAGGED_TERMINAL ?
70             # It would be nice if we could #ifdef HAVE_PERL_VERSION(...)
71             ( $^V ge v5.32 ) ?
72             do { eval 'use experimental "isa"; sub { $_[0] isa String::Tagged }' } :
73             do { require Scalar::Util; sub { Scalar::Util::blessed($_[0]) and $_[0]->isa( "String::Tagged" ) } }
74 8     8   22 : sub { 0 };
75              
76             =head1 CONSTRUCTORS
77              
78             =head2 new
79              
80             $io = IO::Term::Status->new
81              
82             Constructs a new L subclassed instance of this type.
83              
84             =head2 new_for_stdout
85              
86             $io = IO::Term::Status->new_for_stdout
87              
88             Constructs a new instance wrapping the C filehandle, with autoflush
89             turned on. This is usually what you want for printing regular output to the
90             controlling terminal.
91              
92             =cut
93              
94             sub new_for_stdout
95             {
96 0     0 1 0 my $self = shift->new( @_ );
97              
98 0         0 $self->fdopen( STDOUT->fileno, "w" );
99 0         0 $self->autoflush(1);
100              
101 0         0 return $self;
102             }
103              
104             =head1 METHODS
105              
106             =cut
107              
108             sub _build_status
109             {
110 8     8   16 my ( $status ) = @_;
111              
112 8 50       19 if( is_string_tagged( $status ) ) {
    100          
113 0         0 my $termstr = String::Tagged::Terminal->new_from_formatting( $status )
114             ->build_terminal;
115             # Hack the EL in before any SGR reset at the end
116 0 0       0 $termstr =~ s/\e\[m$/$EL\e[m/
117             or $termstr .= $EL;
118 0         0 return $termstr;
119             }
120             elsif( length $status ) {
121 7         64 return $status . $EL;
122             }
123             else {
124 1         6 return "";
125             }
126             }
127              
128             =head2 print_line
129              
130             $io->print_line( @args )
131              
132             Prints a new line from the given arguments, joined as a string. C<@args>
133             should not contain the terminating linefeed.
134              
135             This line is printed above any pending partial line.
136              
137             =cut
138              
139             sub print_line
140             {
141 5     5 1 1053 my $self = shift;
142 5         11 my $partial = ${*$self}{its_partial};
  5         20  
143 5         12 my $status = ${*$self}{its_status};
  5         10  
144              
145 5 100       37 $self->print( join "",
    100          
    100          
    100          
146             ( length $status ? (
147             # Clear the current status first in case the line is wider than the
148             # terminal width
149             ( length $partial ? $CLEARLINE : () ),
150             "\n", $CLEARLINE, $PREVLINE,
151             ) : () ),
152             # Print the new content
153             @_, "\n",
154             ( length $status ? (
155             # Leave an empty space for the partial
156             $CLEARLINE, "\n",
157             # Print the status
158             _build_status( $status ),
159             # Go back and print the partial
160             $PREVLINEHOME
161             ) : () ),
162             ( length $partial ? $partial : () ),
163             );
164             }
165              
166             =head2 more_partial
167              
168             $io->more_partial( $more )
169              
170             Adds more text to the pending partial line displayed at the bottom, after any
171             complete lines.
172              
173             =cut
174              
175             sub more_partial
176             {
177 6     6 1 1740 my $self = shift;
178 6         38 my ( $more ) = @_;
179              
180 6         14 ${*$self}{its_partial} .= $more;
  6         22  
181              
182 6         22 $self->print( $more );
183             }
184              
185             =head2 replace_partial
186              
187             $io->replace_partial( $more )
188              
189             Replace the content of the pending partial line displayed at the bottom.
190              
191             =cut
192              
193             sub replace_partial
194             {
195 1     1 1 23 my $self = shift;
196 1         3 my ( $partial ) = @_;
197              
198 1         2 ${*$self}{its_partial} = $partial;
  1         3  
199              
200 1         4 $self->print( $CLEARLINE . $partial );
201             }
202              
203             =head2 finish_partial
204              
205             $io->finish_partial( $more )
206              
207             Adds more text to the pending partial line then turns it into a complete line
208             that gets printed.
209              
210             =cut
211              
212             sub finish_partial
213             {
214 5     5 1 1452 my $self = shift;
215 5         12 my ( $more ) = @_;
216              
217 5         43 my $status = ${*$self}{its_status};
  5         37  
218              
219 5         10 undef ${*$self}{its_partial};
  5         14  
220              
221 5 100       34 $self->print( join "",
    100          
222             ( length $more ? $more : () ),
223             "\n", $CLEARLINE,
224             ( length $status ? (
225             # Leave an empty space for the partial
226             "\n",
227             # Print the status
228             _build_status( $status ),
229             # Go back and print the partial
230             $PREVLINEHOME
231             ) : () )
232             );
233             }
234              
235             =head2 set_status
236              
237             $io->set_status( $status )
238              
239             Sets the status message string.
240              
241             =cut
242              
243             sub set_status
244             {
245 3     3 1 716 my $self = shift;
246 3         9 my ( $status ) = @_;
247              
248 3         4 my $partial = ${*$self}{its_partial};
  3         17  
249              
250 3         7 ${*$self}{its_status} = $status;
  3         8  
251              
252 3 50       11 $self->print( join "",
253             # Move to status line
254             "\n",
255             # Reprint the status
256             $CLEARLINE,
257             _build_status( $status ),
258             # Go back and print the partial
259             $PREVLINEHOME,
260             ( length $partial ? $partial : () ),
261             );
262             }
263              
264             =head1 AUTHOR
265              
266             Paul Evans
267              
268             =cut
269              
270             0x55AA;