File Coverage

blib/lib/PerlIO/via/EscStatus.pm
Criterion Covered Total %
statement 144 173 83.2
branch 32 58 55.1
condition 12 26 46.1
subroutine 25 27 92.5
pod 2 6 33.3
total 215 290 74.1


line stmt bran cond sub pod time code
1             # Copyright 2008, 2009, 2010, 2011, 2012, 2017 Kevin Ryde
2              
3             # This file is part of PerlIO-via-EscStatus.
4             #
5             # PerlIO-via-EscStatus is free software; you can redistribute it and/or
6             # modify it under the terms of the GNU General Public License as published
7             # by the Free Software Foundation; either version 3, or (at your option) any
8             # later version.
9             #
10             # PerlIO-via-EscStatus is distributed in the hope that it will be useful,
11             # but WITHOUT ANY WARRANTY; without even the implied warranty of
12             # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
13             # Public License for more details.
14             #
15             # You should have received a copy of the GNU General Public License along
16             # with PerlIO-via-EscStatus. If not, see .
17              
18             package PerlIO::via::EscStatus;
19 5     5   3706 use 5.008005; # for unicode properties
  5         25  
20 5     5   29 use strict;
  5         10  
  5         113  
21 5     5   44 use warnings;
  5         10  
  5         137  
22 5     5   26 use Carp;
  5         9  
  5         379  
23 5     5   2195 use Term::Size;
  5         2695  
  5         261  
24 5     5   37 use List::Util qw(min max);
  5         8  
  5         577  
25 5     5   2767 use IO::Handle; # $fh->flush method
  5         30685  
  5         236  
26              
27 5     5   45 use Exporter;
  5         11  
  5         472  
28             our @ISA = ('Exporter');
29             our @EXPORT_OK = qw(ESCSTATUS_STR print_status make_status);
30             our %EXPORT_TAGS = ( all => \@EXPORT_OK );
31              
32 5     5   1819 use PerlIO::via::EscStatus::Parser;
  5         12  
  5         157  
33 5     5   2642 use Regexp::Common 'ANSIescape', 'no_defaults';
  5         21796  
  5         21  
34              
35             our $VERSION = 12;
36              
37             # set this to 1 or 2 for some diagnostics to STDERR
38 5     5   10463 use constant DEBUG => 0;
  5         13  
  5         375  
39              
40              
41             # Flush crib notes:
42             #
43             # IO::Handle::flush(), an xsub, calls PerlIO_flush() (per perlapio) and
44             # returns a status which can be returned by WRITE or CLOSE.
45             #
46             # A "local $|=1" to make print() do a flush (in the style of
47             # IO::Handle::printflush()) gets the flush status incorporated into the
48             # print() success return, but it does "flush/print/flush", and that first
49             # flush is wasteful.
50             #
51             # Believe autoflush $| is always off on the $fh subhandle, irrespective of
52             # whether or not it's set on the top-level. Is that right?
53             #
54             # There's only two places to flush the lower handle: WRITE is the main one,
55             # and POPPED the other (apart from an explicit FLUSH).
56              
57 5         6660 use constant { TABSTOP => 8,
58             ESCSTATUS_STR => "\e_EscStatus\e\\"
59 5     5   42 };
  5         10  
60              
61             #------------------------------------------------------------------------------
62             # public funcs
63              
64             sub print_status {
65 0     0 1 0 return print make_status(@_);
66             }
67              
68             sub make_status {
69 6     6 1 142 my $str = join('',@_);
70 6         19 $str =~ s/^\n+//;
71 6         17 $str =~ s/\n+$//;
72 6         30 $str =~ s/\n/ /g;
73 6         34 return ESCSTATUS_STR . $str . "\n";
74             }
75              
76             #------------------------------------------------------------------------------
77             #
78             # Fields in each instance:
79             #
80             # "display" -- boolean whether to display the status. True when the last
81             # ordinary output char was a newline. Status display is held off until any
82             # line of ordinary output is complete. When first pushed assume we it's the
83             # start of a line.
84             #
85             # "status" -- current status string, or empty string '' for none. This has
86             # been truncated to the tty width (the tty width as of when the status
87             # arrived).
88             #
89             # "status_width" -- the print-width of the "status" string. This can differ
90             # from its length() due to tabs and zero-width and double-width unicode
91             # chars.
92             #
93             # "parser" -- PerlIO::via::EscStatus::Parser object.
94             #
95             # "utf8" -- boolean, initialized by UTF8() below. True when we turned on
96             # the utf8 flag on our layer. This is instance data because there doesn't
97             # seem to be a way for a PerlIO::via module to inspect its own layer flags
98             # later.
99             #
100             sub PUSHED {
101 4     4 0 17642 my ($class, $mode, $fh) = @_;
102 4         7 if (DEBUG) {
103             require Data::Dumper;
104             print STDERR "PUSHED ", Data::Dumper::Dumper ([$class,$mode,$fh]);
105             }
106 4         21 return bless { display => 1,
107             status => '',
108             status_width => 0,
109             parser => PerlIO::via::EscStatus::Parser->new,
110             }, $class;
111             }
112              
113             sub UTF8 {
114 8     8 0 24 my ($self, $belowFlag, $fh) = @_;
115 8         13 if (DEBUG) { print STDERR "UTF8: ",$belowFlag?"yes":"no","\n"; }
116 8         68 return ($self->{'utf8'} = $belowFlag);
117             }
118              
119             # Cribs:
120             # - close() calls CLOSE followed by POPPED
121             # - binmode() removing the layer calls POPPED alone
122             # - if PUSHED returns -1 then POPPED is called with class name and undef,
123             # but that doesn't apply as our push always succeeds
124             #
125             # As of Perl 5.10.0 CLOSE is called after PerlIO::via has closed the
126             # sublayers (with PerlIOBase_close()) in $fh, so unfortunately it's too late
127             # to print an _erase_status(). There's a FLUSH call from PerlIOBase_close()
128             # just before the close, but there's no obvious way to tell it's the
129             # last-ever flush.
130             #
131             # For POPPED must call flush on the sublayers to get the _erase_status() to
132             # show immediately; nothing other happens on the sublayers just because
133             # we're being popped.
134             #
135             sub CLOSE {
136 4     4   41 my ($self, $fh) = @_;
137 4         7 if (DEBUG) { print STDERR "CLOSE() $self $fh\n"; }
138              
139             # no good, $fh already closed
140             # return _erase_status ($self, $fh, 0);
141              
142             # treat as now no status showing
143 4         8 $self->{'status'} = '';
144 4         6 $self->{'status_width'} = 0;
145 4         10 return 0; # success
146             }
147             sub POPPED {
148 4     4 0 8 my ($self, $fh) = @_;
149 4         6 if (DEBUG) { print STDERR "POPPED() $self ", (defined $fh ? $fh : 'undef'), "\n"; }
150 4         12 _erase_status ($self, $fh, 1);
151 4         38 return 0; # always claim success, per perliol(1) docs
152             }
153             # return 0 success, -1 failure
154             sub _erase_status {
155 4     4   5 my ($self, $fh, $want_flush) = @_;
156 4 50 66     24 if ($self->{'display'} && $self->{'status_width'} != 0) {
157 0         0 my $output = "\r" . (' ' x $self->{'status_width'}) . "\r";
158 0         0 $self->{'status'} = '';
159 0         0 $self->{'status_width'} = 0;
160             print $fh $output
161 0 0       0 or do {
162 0         0 if (DEBUG) { print STDERR "_erase_status print error: $!\n"; }
163 0         0 return -1;
164             };
165 0 0       0 if ($want_flush) {
166             $fh->flush()
167 0 0       0 or do {
168 0         0 if (DEBUG) { print STDERR "_erase_status flush error\n"; }
169 0         0 return -1;
170             };
171             }
172             }
173 4         6 return 0;
174             }
175              
176             # As of perl 5.10.0 the default in PerlIO::via is to do nothing if you don't
177             # supply a FLUSH, so chain down explicitly.
178             sub FLUSH {
179 9     9 0 634 my ($self, $fh) = @_;
180 9         18 if (DEBUG) { print STDERR "EscStatus FLUSH $self $fh\n"; }
181 9 50       22 if ($fh) {
182 9         556 return $fh->flush;
183             } else {
184 0         0 return 0; # success
185             }
186             }
187              
188             sub WRITE {
189 2     2   32 my ($self, $buf, $fh) = @_;
190 2         5 my $ret_ok = length ($buf);
191 2         4 if (DEBUG >= 2) {
192             require Data::Dumper;
193             print STDERR "WRITE len=",length($buf),
194             " utf8=",utf8::is_utf8($buf)?"yes":"no",
195             " ", Data::Dumper->new([$buf])->Useqq(1)->Dump;
196             }
197 2         4 my $want_flush = 0;
198              
199 2         6 my $status = $self->{'status'};
200 2         4 my $status_width = $self->{'status_width'};
201              
202 2 50       9 if ($self->{'utf8'}) {
203 0         0 require Encode;
204 0         0 Encode::_utf8_on($buf);
205             }
206 2         8 my ($new_status, $ordinary) = $self->{'parser'}->parse($buf);
207 2         5 my $output = $ordinary;
208              
209 2         3 my $new_status_width;
210 2 100       7 if (defined $new_status) {
211 1         4 ($new_status, $new_status_width)
212             = _truncate ($new_status, _term_width($fh) - 1);
213              
214 1 50       4 if ($new_status eq $status) {
215 0         0 $new_status = undef; # ignore if unchanged
216             }
217             }
218              
219 2 50 66     14 if ($ordinary eq '' && defined $new_status && $self->{'display'}) {
      66        
220             # optimized update of existing status, letting the new overwrite the
221             # old, instead of using all spaces
222 1         6 my $end_len = max (0, $status_width - $new_status_width);
223 1         4 $output = "\r" . $new_status . (' ' x $end_len) . ("\b" x $end_len);
224 1         2 $want_flush = 1;
225 1         2 $self->{'status'} = $new_status;
226 1         2 $self->{'status_width'} = $new_status_width;
227 1         7 goto OUTPUT;
228             }
229              
230 1   33     4 my $want_status_reprint = ($ordinary ne '' || defined $new_status);
231              
232 1 50 33     10 if ($want_status_reprint
      33        
233             && $self->{'display'}
234             && $self->{'status'} ne '') {
235 0 0       0 if (_str_first_line_covers_n ($ordinary, $status_width)) {
236 0         0 $output = "\r" . $output;
237             } else {
238 0         0 $output = "\r" . (' ' x $status_width) . "\r" . $output;
239             }
240             }
241              
242 1 50       4 if (defined $new_status) {
243 0         0 $self->{'status'} = $status = $new_status;
244 0         0 $self->{'status_width'} = $new_status_width;
245             }
246              
247             # if there's some ordinary text being printed then update "display"
248             # if the new text ends with newline then should display status
249 1 50       4 if ($ordinary ne '') {
250 1         4 $self->{'display'} = ($ordinary =~ /\n$/);
251             }
252              
253 1 0 33     4 if ($self->{'display'} && $want_status_reprint && $status ne '') {
      33        
254 0         0 $output .= $status;
255 0         0 $want_flush = 1;
256             }
257              
258             OUTPUT:
259             # Believe for 5.10.0 the utf8 flag should be on the $output string when we
260             # (and the sublayer) are in utf8 mode. Suspect anything seen in the past
261             # contradicting that was due to PerlIO_findFILE() in Term::Size mangling
262             # the whole stack to a :stdio and turning off the utf8 layer flag(s).
263             #
264             # if ($self->{'utf8'}) { Encode::_utf8_off ($output); }
265              
266 2         3 if (DEBUG >= 2) {
267             require Data::Dumper;
268             my $dumper = Data::Dumper->new ([$output]);
269             $dumper->Useqq(1);
270             print STDERR " to lower layer len=",length($output),
271             " utf8=",utf8::is_utf8($output)?"yes":"no",
272             " ", $dumper->Dump;
273             }
274              
275 2 50       29 print $fh $output or return -1;
276 2 50       12 if ($want_flush) { $fh->flush() or return -1; }
  1 100       12  
277 2         13 return $ret_ok;
278             }
279              
280             #------------------------------------------------------------------------------
281              
282             # Zero-width char class.
283             # CR treated as zero width in case it occurs as CRLF.
284             #
285 5         377 use constant IsZero =>
286             "+utf8::Me\n" # mark, enclosing
287             . "+utf8::Mn\n" # mark, non-spacing
288             . "+utf8::Cf\n" # control, format
289             . "-00AD\n" # but exclude soft hyphen which is in Cf
290             . "+0007\n" # BEL
291 5     5   42 . "+000D\n"; # CR, for our purposes
  5         11  
292              
293             # Double-width char class, being East Asian "wide" and "full" chars.
294             # Rumour has it this might be locale-dependent. When turned into a
295             # non-unicode charset there can be slightly different width rules, or
296             # something like that.
297             #
298 5         429 use constant IsDouble =>
299             "+utf8::EastAsianWidth:W\n"
300 5     5   33 . "+utf8::EastAsianWidth:F\n";
  5         36  
301              
302             # "Other" char class, being anything which doesn't introduce one of the
303             # other regexp subexprs, and meaning in practice a single-width char.
304             #
305 5         3700 use constant IsOther =>
306             "!PerlIO::via::EscStatus::IsZero\n"
307             . "-PerlIO::via::EscStatus::IsDouble\n"
308             . "-0009\n" # not a Tab
309             . "-001B\n" # not an Esc
310 5     5   38 . "-0080\t009F\n"; # not an ANSI 8-bit escape, including not CSI
  5         10  
311              
312             # Return true if $str has a complete first line ending in \n and that line
313             # is long enough to overwrite $n chars.
314             sub _str_first_line_covers_n {
315 0     0   0 my ($str, $n) = @_;
316 0 0       0 if ($str !~ /^(.*?)\n/) { return 0; } # not a whole first line
  0         0  
317 0         0 my (undef, $gotlen) = _truncate ($1, $n + 2 * TABSTOP);
318 0         0 return ($gotlen >= $n);
319             }
320              
321             # _truncate() truncates $str to fit in $limit columns.
322             #
323             # The return is two values ($part, $cols). $part is a leading portion of
324             # $str, and possibly later ANSI escapes. $cols is how many columns $part
325             # takes when printed.
326             #
327             # For the common case of a run of single-width ascii chars, there's one
328             # regexp match for the whole lot, then a second notices end of string.
329             #
330             # Text::CharWidth has some similar stuff for IsZero, IsDouble, etc, but
331             # operates on locale byte strings rather than perl wide chars. Not sure if
332             # the width is supposed to be locale-dependent, or just character dependent.
333             # Strictly speaking it depends on the tty anyway.
334             #
335             sub _truncate {
336 405     405   438489 my ($str, $limit) = @_;
337 405         738 my $ret = '';
338 405         604 my $col = 0;
339 405         584 my $overflow = 0;
340              
341 405         3364 while ($str =~ /\G((\p{IsZero}+) # $2
342             |(\p{IsDouble}+) # $3
343             |(\t) # $4
344             |($RE{ANSIescape}) # $5
345             |\p{IsOther}+
346             |. # plain Esc, either non-ANSI or malformed
347             )/gxo) { # o -- compile $RE once
348 507         2374 my $part = $1;
349 507         658 if (DEBUG >= 2) { require Data::Dumper;
350             my $dumper = Data::Dumper->new ([$part]);
351             $dumper->Useqq(1);
352             print STDERR " +$col ",$dumper->Dump; }
353              
354 507 100       1198 if (defined $5) {
355             # an ANSI escape sequence, keep all escape sequences
356 8         12 $ret .= $part;
357 8         30 next;
358             }
359 499 100       967 if ($overflow) {
360             # exclude ordinary chars once overflowed
361 10         31 next;
362             }
363              
364 489 100       1580 if (defined $2) {
    100          
    100          
365             # a run of zero width chars, no change to col
366 55         77 if (DEBUG >= 2) { print STDERR " zero width\n"; }
367              
368             } elsif (defined $3) {
369             # a run of double-width chars
370 6         20 my $room = int (($limit - $col) / 2); # round down
371 6         8 if (DEBUG >= 2) {
372             print STDERR " doubles ".length($part)." in $room\n";
373             }
374 6 100       20 if (length($part) > $room) {
375             # truncate
376 4         14 $part = substr ($part, 0, $room);
377 4         7 $overflow = 1;
378             }
379 6         13 $col += 2 * length($part);
380              
381             } elsif (defined $4) {
382             # a tab (treated one at a time for ease of coding!)
383 2         4 if (DEBUG >= 2) { print STDERR " tab\n"; }
384 2         5 my $newcol = $col + TABSTOP - ($col % TABSTOP);
385 2 50       6 if ($newcol > $limit) {
386 0         0 $overflow = 1;
387 0         0 next;
388             }
389 2         5 $col = $newcol;
390              
391             } else {
392             # a run of single-printing chars, or a single non-ansi Esc or other
393 426         753 my $room = $limit - $col;
394 426         522 if (DEBUG >= 2) {
395             print STDERR " singles ".length($part)." in $room\n";
396             }
397 426 100       909 if (length($part) > $room) {
398             # truncate
399 7         14 $part = substr ($part, 0, $room);
400 7         8 $overflow = 1;
401             }
402 426         733 $col += length($part);
403             }
404              
405 489         1589 $ret .= $part;
406             }
407              
408 405         1891 if (DEBUG >= 2) { require Data::Dumper;
409             my $dumper = Data::Dumper->new ([$ret]);
410             $dumper->Useqq(1);
411             print STDERR " ret $col ",$dumper->Dump; }
412 405         1383 return ($ret, $col);
413             }
414              
415             # This _term_width() is a nasty hack for perl 5.10.0 where PerlIO_findFILE()
416             # as used by Term::Size 0.2, through the "FILE*" typemap, clears the :utf8
417             # flag on a perlio layer. Not sure if that clearing is a bug or a feature.
418             # It might be a feature in that you lose translations when going to raw
419             # stdio. In any case until Term::Size uses PerlIO_fileno() have a
420             # workaround here with a temporary stream on a dup-ed fileno() of $fh to
421             # keep the original safe from harm.
422             #
423             # There's probably plenty of other strategies for an idea of "print width"
424             # on a stream. Some sort of property of the whole stream, or per-layer,
425             # which could be overridden when you want wider or narrower output no matter
426             # what the underlying fd claims (eg. from a "COLUMNS" envvar) ...
427             #
428             # Note: If $fh is only for read then '>&' mode makes $tmp give a FILE* as
429             # NULL, which seg-faults with Term::Size 0.2. Should be output-only in the
430             # uses from WRITE, but wouldn't mind guarding against that, or depending on
431             # a better Term::Size.
432             #
433             sub _term_width {
434 3     3   7693 my ($fh) = @_;
435 3         5 my $width;
436 3         8 my $fd = fileno($fh);
437 3         5 if (DEBUG >= 2) { print STDERR "_term_width on fd=",
438             (defined $fd ? $fd : 'undef'), "\n"; }
439 3 50       9 if (defined $fd) {
440 3 50       77 if (open my $tmp, '>&', $fd) {
441 3         22 $width = Term::Size::chars($tmp);
442 3 50       30 close $tmp or die;
443             }
444             }
445 3   50     19 return ($width || 80);
446             }
447              
448             1;
449             __END__