File Coverage

blib/lib/String/Tagged/Terminal.pm
Criterion Covered Total %
statement 72 81 88.8
branch 35 48 72.9
condition 24 40 60.0
subroutine 14 16 87.5
pod 5 5 100.0
total 150 190 78.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, 2017-2023 -- leonerd@leonerd.org.uk
5              
6             package String::Tagged::Terminal 0.06;
7              
8 5     5   903659 use v5.14;
  5         40  
9 5     5   29 use warnings;
  5         10  
  5         145  
10              
11 5     5   29 use base qw( String::Tagged );
  5         8  
  5         3002  
12              
13 5     5   37328 use constant HAVE_MSWIN32 => $^O eq "MSWin32";
  5         16  
  5         5450  
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             =head2 sizepos
68              
69             (experimental)
70              
71             This tag takes a value indicating an adjustment to the vertical positioning,
72             and possibly also size, in order to create subscript or superscript effects.
73              
74             Recognised values are C for subscript, and C for superscript.
75             These are implemented using the F-style C codes.
76              
77             =cut
78              
79             =head1 CONSTRUCTORS
80              
81             =cut
82              
83             =head2 new_from_formatting
84              
85             $st = String::Tagged::Terminal->new_from_formatting( $fmt )
86              
87             Returns a new instance by converting L standard
88             tags.
89              
90             Foreground and background colours are converted to their nearest index in the
91             xterm 256 colour palette. The C Formatting attribute is rendered by
92             selecting the first alternate font using C.
93              
94             =cut
95              
96             sub new_from_formatting
97             {
98 2     2 1 54082 my $class = shift;
99 2         9 my ( $orig ) = @_;
100              
101 2         15 require Convert::Color::XTerm;
102              
103             return $class->clone( $orig,
104             only_tags => [qw(
105             bold under italic strike blink reverse sizepos
106             monospace
107             fg bg
108             )],
109             convert_tags => {
110 1 50   1   104 monospace => sub { $_[1] ? ( altfont => 1 ) : () },
111              
112 1     1   124 fg => sub { fgindex => $_[1]->as_xterm->index },
113 0     0   0 bg => sub { bgindex => $_[1]->as_xterm->index },
114             },
115 2         39 );
116             }
117              
118             =head1 METHODS
119              
120             The following methods are provided in addition to those provided by
121             L.
122              
123             =cut
124              
125             =head2 build_terminal
126              
127             $str = $st->build_terminal( %opts )
128              
129             Returns a string containing terminal escape sequences mixed with string
130             content to render the string to a terminal.
131              
132             As this string will contain literal terminal control escape sequences, care
133             should be taken when passing it around, printing it for debugging purposes, or
134             similar.
135              
136             Takes the following additional named options:
137              
138             =over 4
139              
140             =item no_color
141              
142             If true, the C and C attributes will be ignored. This has
143             the result of performing some formatting using the other attributes, but not
144             setting colours.
145              
146             =back
147              
148             =cut
149              
150             sub build_terminal
151             {
152 13     13 1 9855 my $self = shift;
153 13         31 my %opts = @_;
154              
155 13         26 my $ret = "";
156 13         19 my %pen;
157             $self->iter_substr_nooverlap( sub {
158 33     33   1917 my ( $s, %tags ) = @_;
159              
160 33         68 my @sgr;
161              
162             # Simple boolean attributes first
163 33         154 foreach (
164             [ bold => 1, 22 ],
165             [ under => 4, 24 ],
166             [ italic => 3, 23 ],
167             [ strike => 9, 29 ],
168             [ blink => 5, 25 ],
169             [ reverse => 7, 27 ],
170             ) {
171 198         336 my ( $tag, $on, $off ) = @$_;
172              
173 198 100 66     387 push( @sgr, $on ), $pen{$tag} = 1 if $tags{$tag} and !$pen{$tag};
174 198 100 100     582 push( @sgr, $off ), delete $pen{$tag} if !$tags{$tag} and $pen{$tag};
175             }
176              
177             # Numerical attributes
178 33         103 foreach (
179             [ altfont => 10, 9 ],
180             ) {
181 33         65 my ( $tag, $base, $max ) = @$_;
182              
183 33 100 66     145 if( defined $pen{$tag} and !defined $tags{$tag} ) {
    50 33        
    100 33        
184 2         4 push @sgr, $base;
185 2         5 delete $pen{$tag};
186             }
187             elsif( defined $pen{$tag} and defined $tags{$tag} and $pen{$tag} == $tags{$tag} ) {
188             # Leave it
189             }
190             elsif( defined $tags{$tag} ) {
191 2         4 my $val = $tags{$tag};
192 2 50       6 $val = $max if $val > $max;
193 2         5 push @sgr, $base + $val;
194 2         5 $pen{$tag} = $val;
195             }
196             }
197              
198             # Colour index attributes
199 33         93 foreach (
200             [ fgindex => 30 ],
201             [ bgindex => 40 ],
202             ) {
203 66         122 my ( $tag, $base ) = @$_;
204 66         95 my $val = $tags{$tag};
205 66 100       127 $val = int $val if defined $val;
206              
207 66 50 66     265 if( defined $pen{$tag} and !defined $val ) {
    50 66        
    100 66        
208             # Turn it off
209 0         0 push @sgr, $base + 9;
210 0         0 delete $pen{$tag};
211             }
212             elsif( defined $pen{$tag} and defined $val and $pen{$tag} == $val ) {
213             # Leave it
214             }
215             elsif( defined $val ) {
216 7 100       17 if( $val < 8 ) {
    100          
217             # VGA 8
218 5         9 push @sgr, $base + $val;
219             }
220             elsif( $val < 16 ) {
221             # Hi 16
222 1         4 push @sgr, $base + 60 + ( $val - 8 );
223             }
224             else {
225             # Xterm256 palette 5 = 256 colours
226 1         7 push @sgr, sprintf "%d:%d:%d", $base + 8, 5, $val;
227             }
228 7         15 $pen{$tag} = $val;
229             }
230             }
231              
232             {
233 33 50 33     56 if( defined $pen{sizepos} and !defined $tags{sizepos} ) {
  33 50 33     148  
    50 33        
234 0         0 push @sgr, 75; # reset
235             }
236             elsif( defined $pen{sizepos} and defined $tags{sizepos} and $pen{sizepos} eq $tags{sizepos} ) {
237             # Leave it
238             }
239             elsif( defined( my $val = $tags{sizepos} ) ) {
240 0 0       0 if( $val eq "sub" ) {
    0          
241 0         0 push @sgr, 74;
242             }
243             elsif( $val eq "super" ) {
244 0         0 push @sgr, 73;
245             }
246 0         0 $pen{sizepos} = $val;
247             }
248             }
249              
250 33 100 100     110 if( @sgr and %pen ) {
    100          
251 15         49 $ret .= "\e[" . join( ";", @sgr ) . "m";
252             }
253             elsif( @sgr ) {
254 7         13 $ret .= "\e[m";
255             }
256              
257 33         112 $ret .= $s;
258             },
259 13 100       118 ( $opts{no_color} ? ( except => [qw( fgindex bgindex )] ) : () ) );
260              
261 13 100       333 $ret .= "\e[m" if %pen;
262              
263 13         74 return $ret;
264             }
265              
266             =head2 as_formatting
267              
268             $fmt = $st->as_formatting
269              
270             Returns a new C instance tagged with
271             L standard tags.
272              
273             =cut
274              
275             sub as_formatting
276             {
277 2     2 1 1140 my $self = shift;
278              
279 2         13 require Convert::Color::XTerm;
280              
281             return String::Tagged->clone( $self,
282             only_tags => [qw(
283             bold under italic strike blink reverse sizepos
284             altfont
285             fgindex bgindex
286             )],
287             convert_tags => {
288 1 50   1   87 altfont => sub { $_[1] == 1 ? ( monospace => 1 ) : () },
289              
290 1     1   98 fgindex => sub { fg => Convert::Color::XTerm->new( $_[1] ) },
291 0     0   0 bgindex => sub { bg => Convert::Color::XTerm->new( $_[1] ) },
292             },
293 2         25 );
294             }
295              
296             =head2 print_to_terminal
297              
298             $str->print_to_terminal( $fh )
299              
300             I
301              
302             Prints the string to the terminal by building a terminal escape string then
303             printing it to the given IO handle (or C if not supplied).
304              
305             This method will pass the value of the C environment variable to the
306             underlying L method call, meaning if that has a true value
307             then colouring tags will be ignored, yielding a monochrome output. This
308             follows the suggestion of L.
309              
310             =cut
311              
312             sub print_to_terminal
313             {
314 5     5 1 14404 my $self = shift;
315 5         17 my ( $fh, %options ) = @_;
316              
317 5   100     35 $fh //= \*STDOUT;
318              
319 5         17 $options{win32}++ if HAVE_MSWIN32 and not exists $options{win32};
320              
321 5 100       19 if( $options{win32} ) {
322 3         22 $self->String::Tagged::Terminal::Win32Console::print_to_console( $fh, %options );
323             }
324             else {
325 2         11 $fh->print( $self->build_terminal( no_color => $ENV{NO_COLOR} ) );
326             }
327             }
328              
329             =head2 say_to_terminal
330              
331             $str->say_to_terminal( $fh )
332              
333             I
334              
335             Prints the string to the terminal as per L, followed by a
336             linefeed.
337              
338             =cut
339              
340             sub say_to_terminal
341             {
342 1     1 1 6249 my $self = shift;
343 1         3 my ( $fh, %options ) = @_;
344              
345 1   50     5 $fh //= \*STDOUT;
346              
347 1         5 $self->print_to_terminal( $fh, %options );
348 1         19 $fh->say;
349             }
350              
351             =head1 COMPATIBILITY NOTES
352              
353             On Windows, the following notes apply:
354              
355             =over 4
356              
357             =item *
358              
359             On all versions of Windows, the attributes C, C and C
360             are supported. The C attribute is implemented by using high-intensity
361             colours, so will be indistinguishable from using high-intensity colour indexes
362             without bold. The full 256-color palette is not supported by Windows, so it is
363             down-converted to the 16 colours that are.
364              
365             =item *
366              
367             Starting with Windows 10, also C and C are supported.
368              
369             =item *
370              
371             The attributes C, C, C, C are not supported on
372             any Windows version.
373              
374             =item *
375              
376             On Windows, only a single output console is supported.
377              
378             =back
379              
380             =head1 TODO
381              
382             =over 4
383              
384             =item *
385              
386             Consider a C<< ->parse_terminal >> constructor method, which would attempt to
387             parse SGR sequences from a given source string.
388              
389             =back
390              
391             =cut
392              
393             =head1 AUTHOR
394              
395             Paul Evans
396              
397             =cut
398              
399             0x55AA;