File Coverage

blib/lib/String/Tagged/Terminal.pm
Criterion Covered Total %
statement 70 74 94.5
branch 32 38 84.2
condition 21 31 67.7
subroutine 14 16 87.5
pod 5 5 100.0
total 142 164 86.5


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