#!/usr/bin/env perl ########################## # Scott Baker # 2017-06-29 ########################## use strict; use warnings; use Data::Dump::Color; use Test::More; use v5.16; ############################################################################### ############################################################################### if (@ARGV && $ARGV[0] eq '--demo') { color_demo(); print "\n"; exit; } is(ansi_reveal(color("reset")) , "[RESET]" , "Reset"); is(ansi_reveal(color("bold")) , "[BOLD]" , "Bold"); is(ansi_reveal(color("blink")) , "[SLOWBLINK]", "Blink"); is(ansi_reveal(color("italic")) , "[ITALIC]" , "Italic"); is(ansi_reveal(color("underline")), "[UNDERLINE]", "Underline"); is(ansi_reveal(color("inverse")) , "[INVERSE]" , "Inverse"); is(ansi_reveal(color("red")) , "[FG-160]" , "Color: Red"); is(ansi_reveal(color("blue")) , "[FG-27]" , "Color: Blue"); is(ansi_reveal(color("green")) , "[FG-34]" , "Color: Green"); is(ansi_reveal(color("yellow")) , "[FG-226]" , "Color: Yellow"); is(ansi_reveal(color("orange")) , "[FG-214]" , "Color: Orange"); is(ansi_reveal(color("purple")) , "[FG-93]" , "Color: Purple"); is(ansi_reveal(color("white")) , "[FG-15]" , "Color: White"); is(ansi_reveal(color("black")) , "[FG-0]" , "Color: Black"); is(ansi_reveal(color("red_bold")) , "[FG-160][BOLD]" , "Color: Bold Red"); is(ansi_reveal(color("213_italic")) , "[FG-213][ITALIC]" , "Color: 213 Italic"); is(ansi_reveal(color("red_on_blue")), "[FG-160][BG-27]" , "Color: Red on Blue"); is(ansi_reveal(color("165_blink")) , "[FG-165][SLOWBLINK]", "Color: 164 Blink"); is(ansi_reveal(color("on_white")) , "[BG-15]" , "Color: On White"); is(ansi_reveal(color(160, '0')) , '[FG-160]0[RESET]' , "Colored text '0'"); is(ansi_reveal(color(123, '1')) , '[FG-123]1[RESET]' , "Colored text '1'"); is(ansi_reveal(color(99 , '')) , '[FG-99][RESET]' , "Colored text ''"); is(ansi_reveal(color(82 , 'Scott')), '[FG-82]Scott[RESET]', "Colored text 'Scott'"); is(ansi_reveal(color(66)) , '[FG-66]', "Number only integer"); is(ansi_reveal(color("67")), '[FG-67]', "Number only string"); #is(ansi_reveal(color(670)) , '' , "Number > 255 (not valid)"); is(ansi_reveal(color(-11)) , '[BG-11]', "Negative number -11"); is(ansi_reveal(color("#aabbcc")) , '[FG-#AABBCC]' , "RGB: #aabbcc"); is(ansi_reveal(color("#123456")) , '[FG-#123456]' , "RGB: #123456"); is(ansi_reveal(color("on_#123456")) , '[BG-#123456]' , "RGB: on_#123456"); is(ansi_reveal(color("#123456" , 'scott')), '[FG-#123456]scott[RESET]', "RGB: #123456 + text"); is(ansi_reveal(color("on_#123456", 'scott')), '[BG-#123456]scott[RESET]', "RGB: on_#123456 + text"); done_testing(); ############################################################################### ############################################################################### sub color_demo { my @colors = qw(red blue green yellow orange purple white black); for (my $i = 0; $i < 3; $i++) { foreach my $color (sort @colors) { my $ansi = color($color); print color("bold") . $ansi . "XXXXXX"; } print color() . "\n"; } print color("bold") . color("yellow") . " Scott Baker " . color() . "\n"; print color("83_bold") . " Scott Baker " . color() . "\n"; print color("blink") . color("198") . " Blink Baker " . color() . "\n"; print color("underline") . color("57") . " Underline Baker " . color() . "\n"; print color("italic") . color("26") . " Italic Baker " . color() . "\n"; print color("inverse") . color("26") . " Invert Baker " . color() . "\n"; print color("213_italic") . " Scott pink italic" . color() . "\n"; print color("green_bold") . " Green Baker " . color() . "\n"; print color("black_bold") . color("inverse") . " Inverse Baker " . color() . "\n"; print color("bold") . color("yellow_on_blue") . " SUPERMAN " . color() . "\n"; print color("bold") . color("red_blink") . " RED BLINK " . color() . "\n"; } sub bleach_text { my $str = shift(); $str =~ s/\e\[\d*(;\d+)*m//g; return $str; } # Debug print variable using either Data::Dump::Color (preferred) or Data::Dumper # Creates methods k() and kd() to print, and print & die respectively BEGIN { if (eval { require Data::Dump::Color }) { *k = sub { Data::Dump::Color::dd(@_) }; } else { require Data::Dumper; *k = sub { print Data::Dumper::Dumper(\@_) }; } sub kd { k(@_); printf("Died at %2\$s line #%3\$s\n",caller()); exit(15); } } sub ansi_numbers { my $str = shift(); my @p = split(/;/, $str); my @ret = (); #k(\@p); for (my $i = 0; $i < scalar(@p); $i++) { my $x = $p[$i]; my $y = int($p[$i + 1] || 0); my $z = int($p[$i + 2] || 0); my $r = $p[$i + 2] || ""; my $g = $p[$i + 3] || ""; my $b = $p[$i + 4] || ""; if ($x == 0) { push(@ret, 'RESET'); } elsif ($x == 1) { push(@ret, 'BOLD'); } elsif ($x == 3) { push(@ret, 'ITALIC'); } elsif ($x == 4) { push(@ret, 'UNDERLINE'); } elsif ($x == 5) { push(@ret, 'SLOWBLINK'); } elsif ($x == 6) { push(@ret, 'FASTBLINK'); } elsif ($x == 7) { push(@ret, 'INVERSE'); # 4bit } elsif ($x >= 30 && $x < 38) { push(@ret, "FCOLOR-" . ($x - 30)); } elsif ($x >= 90 && $x < 98) { push(@ret, "FCOLOR-" . ($x - 82)); } elsif ($x >= 40 && $x < 48) { push(@ret, "BCOLOR-" . ($x - 40)); } elsif ($x >= 100 && $x < 108) { push(@ret, "BCOLOR-" . ($x - 92)); # 8bit } elsif ($x == 38 && $y == 5) { push(@ret, "FG-$z"); $i += 2; } elsif ($x == 48 && $y == 5) { push(@ret, "BG-$z"); $i += 2; # 24bit } elsif ($x == 38 && $y == 2) { push(@ret, sprintf("FG-#%X%X%X", $r, $g, $b)); $i += 4; } elsif ($x == 48 && $y == 2) { push(@ret, sprintf("BG-#%X%X%X", $r, $g, $b)); $i += 4; } else { push(@ret, "??$x??"); } } # Quote each item foreach (@ret) { $_ = "[$_]"; } my $ret = join("", @ret); return $ret; } sub ansi_reveal { my $str = shift(); my $raw = $str; # Make the escape chars visible $raw =~ s/\e\[(.*?)m/ESC[$1]/g; # Convert the numbers to english #k($raw); $raw =~ s/ESC\[(.*?)\]/ansi_numbers($1)/eg; return $raw; } ########################################################################################## ########################################################################################## ########################################################################################## # String format: '115', '165_bold', '10_on_140', 'reset', 'on_173', 'red', 'white_on_blue' sub color { my ($str, $txt) = @_; if (-t STDOUT == 0 || $ENV{NO_COLOR}) { return $txt // ""; } # No interactive terminal if (!length($str) || $str eq 'reset') { return "\e[0m"; } # No string = RESET # Some predefined colors/commands state %color_map = qw(red 160 blue 27 green 34 yellow 226 orange 214 purple 93 white 15 black 0); state %cmd_map = qw(bold 1 italic 3 underline 4 blink 5 inverse 7); # Pre-process the string $str =~ s/on_/-/g; # "on_" becomes a negative number $str =~ s|([A-Za-z]+)|$color_map{$1} // $1|eg; # color name -> number my @parts = map { # If it's negative it's a background color, otherwise foreground $cmd_map{$_} // ($_ =~ /^-(.+)/ ? get_ansi_color($1, 48) : get_ansi_color($_, 38)) } split("_", $str); my $ret = "\e[" . join(";", @parts) . "m"; if (defined $txt) { $ret .= $txt . "\e[0m"; } return $ret; } # String format: '115', '165_bold', '10_on_140', 'reset', 'on_173', 'red', 'white_on_blue' sub color_old1 { my ($str, $txt) = @_; if (-t STDOUT == 0 || $ENV{NO_COLOR}) { return $txt // ""; } # No interactive terminal if (!length($str) || $str eq 'reset') { return "\e[0m"; } # No string = RESET # Some predefined colors/commands my %color_map = qw(red 160 blue 27 green 34 yellow 226 orange 214 purple 93 white 15 black 0); my %cmd_map = qw(bold 1 italic 3 underline 4 blink 5 inverse 7); # Pre-process the string. $str =~ s/on_/-/; # "on_" becomes a negative number $str =~ s|([A-Za-z]+)|$color_map{$1} // $1|eg; # command number my @parts = split("_", $str); foreach my $p (@parts) { my $cmd_num = $cmd_map{$p // 0}; if ($cmd_num) { $p = $cmd_num; } elsif (defined($p) && $p =~ /^-(.+)/) { $p = "48;5;$p"; } elsif (defined($p)) { $p = "38;5;$p"; } } my $ret = "\e[" . join(";", @parts) . "m"; if (defined($txt)) { $ret .= $txt . "\e[0m"; } return $ret; } sub get_ansi_color { my ($str, $mode) = @_; # params: (number/#rgb, 38/48 foreground/background) return $str =~ m/#(\w{6})$/ ? do { my ($r,$g,$b) = unpack 'C3', pack 'H6', $1; "$mode;2;$r;$g;$b" } : $mode . ";5;" . $str; } sub get_ansi_color_full { my ($str, $mode) = @_; if ($str =~ m/#(\w{6})/) { my ($r,$g,$b) = unpack('C3', (pack 'H6', $1)); return "$mode;2;$r;$g;$b"; } else { return "$mode;5;$str"; } }