File Coverage

blib/lib/String/Tagged/Terminal.pm
Criterion Covered Total %
statement 72 76 94.7
branch 30 36 83.3
condition 21 31 67.7
subroutine 15 17 88.2
pod 5 5 100.0
total 143 165 86.6


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, 2017-2018 -- leonerd@leonerd.org.uk
5              
6             package String::Tagged::Terminal;
7              
8 5     5   222401 use strict;
  5         44  
  5         124  
9 5     5   27 use warnings;
  5         8  
  5         101  
10 5     5   86 use 5.010; # //
  5         15  
11              
12 5     5   23 use base qw( String::Tagged );
  5         8  
  5         2694  
13              
14 5     5   33426 use constant HAVE_MSWIN32 => $^O eq "MSWin32";
  5         11  
  5         4064  
15             HAVE_MSWIN32 and require String::Tagged::Terminal::Win32Console;
16              
17             our $VERSION = '0.04';
18              
19             require IO::Handle;
20              
21             =head1 NAME
22              
23             C - format terminal output using C
24              
25             =head1 SYNOPSIS
26              
27             use String::Tagged::Terminal;
28              
29             my $st = String::Tagged::Terminal->new
30             ->append( "Hello my name is " )
31             ->append_tagged( $name, bold => 1, fgindex => 4 );
32              
33             $st->say_to_terminal;
34              
35             =head1 DESCRIPTION
36              
37             This subclass of L provides a method, C,
38             for outputting the formatting tags embedded in the string as terminal escape
39             sequences, to render the the output in the appropriate style.
40              
41             =head1 TAGS
42              
43             The following tag names are recognised:
44              
45             =head2 bold, under, italic, strike, blink, reverse
46              
47             These tags take a boolean value. If the value is true then the corresponding
48             terminal rendering attribute is enabled.
49              
50             =head2 altfont
51              
52             This tag takes an integer value. If defined it uses the "alternate font
53             selection" sequence.
54              
55             =head2 fgindex, bgindex
56              
57             These tags take an integer value in the range 0 to 255. These select the
58             foreground or background colour by using VGA, high-brightness extended 16
59             colour, or xterm 256 palette mode attributes, depending on the value.
60              
61             The ECMA-48-corrected string encoding form of C is used to set
62             the 256 palette values.
63              
64             =cut
65              
66             =head1 CONSTRUCTORS
67              
68             =cut
69              
70             =head2 new_from_formatting
71              
72             $st = String::Tagged::Terminal->new_from_formatting( $fmt )
73              
74             Returns a new instance by converting L standard
75             tags.
76              
77             Foreground and background colours are converted to their nearest index in the
78             xterm 256 colour palette. The C Formatting attribute is rendered by
79             selecting the first alternate font using C.
80              
81             =cut
82              
83             sub new_from_formatting
84             {
85 2     2 1 48035 my $class = shift;
86 2         6 my ( $orig ) = @_;
87              
88 2         13 require Convert::Color::XTerm;
89              
90             return $class->clone( $orig,
91             only_tags => [qw(
92             bold under italic strike blink reverse
93             monospace
94             fg bg
95             )],
96             convert_tags => {
97 1 50   1   89 monospace => sub { $_[1] ? ( altfont => 1 ) : () },
98              
99 1     1   104 fg => sub { fgindex => $_[1]->as_xterm->index },
100 0     0   0 bg => sub { bgindex => $_[1]->as_xterm->index },
101             },
102 2         36 );
103             }
104              
105             =head1 METHODS
106              
107             The following methods are provided in addition to those provided by
108             L.
109              
110             =cut
111              
112             =head2 build_terminal
113              
114             $str = $st->build_terminal( %opts )
115              
116             Returns a string containing terminal escape sequences mixed with string
117             content to render the string to a terminal.
118              
119             As this string will contain literal terminal control escape sequences, care
120             should be taken when passing it around, printing it for debugging purposes, or
121             similar.
122              
123             Takes the following additional named options:
124              
125             =over 4
126              
127             =item no_color
128              
129             If true, the C and C attributes will be ignored. This has
130             the result of performing some formatting using the other attributes, but not
131             setting colours.
132              
133             =back
134              
135             =cut
136              
137             sub build_terminal
138             {
139 12     12 1 3988 my $self = shift;
140 12         25 my %opts = @_;
141              
142 12         18 my $ret = "";
143 12         15 my %pen;
144             $self->iter_substr_nooverlap( sub {
145 31     31   1412 my ( $s, %tags ) = @_;
146              
147 31         36 my @sgr;
148              
149             # Simple boolean attributes first
150 31         99 foreach (
151             [ bold => 1, 22 ],
152             [ under => 4, 24 ],
153             [ italic => 3, 23 ],
154             [ strike => 9, 29 ],
155             [ blink => 5, 25 ],
156             [ reverse => 7, 27 ],
157             ) {
158 186         255 my ( $tag, $on, $off ) = @$_;
159              
160 186 100 66     324 push( @sgr, $on ), $pen{$tag} = 1 if $tags{$tag} and !$pen{$tag};
161 186 100 100     480 push( @sgr, $off ), delete $pen{$tag} if !$tags{$tag} and $pen{$tag};
162             }
163              
164             # Numerical attributes
165 31         79 foreach (
166             [ altfont => 10, 9 ],
167             ) {
168 31         56 my ( $tag, $base, $max ) = @$_;
169              
170 31 100 66     115 if( defined $pen{$tag} and !defined $tags{$tag} ) {
    50 33        
    100 33        
171 2         4 push @sgr, $base;
172 2         3 delete $pen{$tag};
173             }
174             elsif( defined $pen{$tag} and defined $tags{$tag} and $pen{$tag} == $tags{$tag} ) {
175             # Leave it
176             }
177             elsif( defined $tags{$tag} ) {
178 2         3 my $val = $tags{$tag};
179 2 50       5 $val = $max if $val > $max;
180 2         3 push @sgr, $base + $val;
181 2         4 $pen{$tag} = $val;
182             }
183             }
184              
185             # Colour index attributes
186 31         68 foreach (
187             [ fgindex => 30 ],
188             [ bgindex => 40 ],
189             ) {
190 62         81 my ( $tag, $base ) = @$_;
191              
192 62 50 66     205 if( defined $pen{$tag} and !defined $tags{$tag} ) {
    50 66        
    100 66        
193             # Turn it off
194 0         0 push @sgr, $base + 9;
195 0         0 delete $pen{$tag};
196             }
197             elsif( defined $pen{$tag} and defined $tags{$tag} and $pen{$tag} == $tags{$tag} ) {
198             # Leave it
199             }
200             elsif( defined $tags{$tag} ) {
201 6         10 my $val = $tags{$tag};
202 6 100       11 if( $val < 8 ) {
    100          
203             # VGA 8
204 4         7 push @sgr, $base + $val;
205             }
206             elsif( $val < 16 ) {
207             # Hi 16
208 1         3 push @sgr, $base + 60 + ( $val - 8 );
209             }
210             else {
211             # Xterm256 palette 5 = 256 colours
212 1         5 push @sgr, sprintf "%d:%d:%d", $base + 8, 5, $val;
213             }
214 6         11 $pen{$tag} = $val;
215             }
216             }
217              
218 31 100 100     94 if( @sgr and %pen ) {
    100          
219 14         36 $ret .= "\e[" . join( ";", @sgr ) . "m";
220             }
221             elsif( @sgr ) {
222 7         11 $ret .= "\e[m";
223             }
224              
225 31         93 $ret .= $s;
226             },
227 12 100       95 ( $opts{no_color} ? ( except => [qw( fgindex bgindex )] ) : () ) );
228              
229 12 100       213 $ret .= "\e[m" if %pen;
230              
231 12         60 return $ret;
232             }
233              
234             =head2 as_formatting
235              
236             $fmt = $st->as_formatting
237              
238             Returns a new C instance tagged with
239             L standard tags.
240              
241             =cut
242              
243             sub as_formatting
244             {
245 2     2 1 1129 my $self = shift;
246              
247 2         10 require Convert::Color::XTerm;
248              
249             return String::Tagged->clone( $self,
250             only_tags => [qw(
251             bold under italic strike blink reverse
252             altfont
253             fgindex bgindex
254             )],
255             convert_tags => {
256 1 50   1   68 altfont => sub { $_[1] == 1 ? ( monospace => 1 ) : () },
257              
258 1     1   74 fgindex => sub { fg => Convert::Color::XTerm->new( $_[1] ) },
259 0     0   0 bgindex => sub { bg => Convert::Color::XTerm->new( $_[1] ) },
260             },
261 2         19 );
262             }
263              
264             =head2 print_to_terminal
265              
266             $str->print_to_terminal( $fh )
267              
268             I
269              
270             Prints the string to the terminal by building a terminal escape string then
271             printing it to the given IO handle (or C if not supplied).
272              
273             This method will pass the value of the C environment variable to the
274             underlying L method call, meaning if that has a true value
275             then colouring tags will be ignored, yielding a monochrome output. This
276             follows the suggestion of L.
277              
278             =cut
279              
280             sub print_to_terminal
281             {
282 5     5 1 4303 my $self = shift;
283 5         9 my ( $fh, %options ) = @_;
284              
285 5   100     21 $fh //= \*STDOUT;
286              
287 5         11 $options{win32}++ if HAVE_MSWIN32 and not exists $options{win32};
288              
289 5 100       12 if( $options{win32} ) {
290 3         15 $self->String::Tagged::Terminal::Win32Console::print_to_console( $fh, %options );
291             }
292             else {
293 2         8 $fh->print( $self->build_terminal( no_color => $ENV{NO_COLOR} ) );
294             }
295             }
296              
297             =head2 say_to_terminal
298              
299             $str->say_to_terminal( $fh )
300              
301             I
302              
303             Prints the string to the terminal as per L, followed by a
304             linefeed.
305              
306             =cut
307              
308             sub say_to_terminal
309             {
310 1     1 1 710 my $self = shift;
311 1         3 my ( $fh, %options ) = @_;
312              
313 1   50     3 $fh //= \*STDOUT;
314              
315 1         6 $self->print_to_terminal( $fh, %options );
316 1         13 $fh->say;
317             }
318              
319             =head1 COMPATIBILITY NOTES
320              
321             On Windows, the following notes apply:
322              
323             =over 4
324              
325             =item *
326              
327             On all versions of Windows, the attributes C, C and C
328             are supported. The C attribute is implemented by using high-intensity
329             colours, so will be indistinguishable from using high-intensity colour indexes
330             without bold. The full 256-color palette is not supported by Windows, so it is
331             down-converted to the 16 colours that are.
332              
333             =item *
334              
335             Starting with Windows 10, also C and C are supported.
336              
337             =item *
338              
339             The attributes C, C, C, C are not supported on
340             any Windows version.
341              
342             =item *
343              
344             On Windows, only a single output console is supported.
345              
346             =back
347              
348             =head1 TODO
349              
350             =over 4
351              
352             =item *
353              
354             Consider a C<< ->parse_terminal >> constructor method, which would attempt to
355             parse SGR sequences from a given source string.
356              
357             =back
358              
359             =cut
360              
361             =head1 AUTHOR
362              
363             Paul Evans
364              
365             =cut
366              
367             0x55AA;