-.TH COLORIZE 1 "2019-09-01" "colorize v0.66" "User Commands"
+.TH COLORIZE 1 "2024-07-25" "colorize v0.66" "User Commands"
.SH NAME
colorize \- colorize text on terminal with ANSI escape sequences
.SH SYNOPSIS
.BR \-\-omit\-color\-empty
omit printing color escape sequences for empty lines
.TP
+.BR \-\-rainbow\-fg
+enable foreground color rainbow mode
+.TP
.BR \-h ", " \-\-help
show help screen and exit
.TP
color = magenta # favorite one
exclude-random = black
omit-color-empty = yes
+rainbow-fg = no
.fi
.RE
.RE
color (value same as command-line colors)
exclude-random (value same as command-line option)
omit-color-empty (yes/no)
+rainbow-fg (yes/no)
.fi
.RE
.RE
char *color;
char *exclude_random;
char *omit_color_empty;
+ char *rainbow_fg;
};
enum { DESC_OPTION, DESC_CONF };
struct color {
const char *name;
const char *code;
+ unsigned int index;
};
+static unsigned int rainbow_index;
+
static const struct color fg_colors[] = {
- { "none", NULL },
- { "black", "30m" },
- { "red", "31m" },
- { "green", "32m" },
- { "yellow", "33m" },
- { "blue", "34m" },
- { "magenta", "35m" },
- { "cyan", "36m" },
- { "white", "37m" },
- { "default", "39m" },
+ { "none", NULL, 0 },
+ { "black", "30m", 1 },
+ { "red", "31m", 2 },
+ { "green", "32m", 3 },
+ { "yellow", "33m", 4 },
+ { "blue", "34m", 5 },
+ { "magenta", "35m", 6 },
+ { "cyan", "36m", 7 },
+ { "white", "37m", 8 },
+ { "default", "39m", 9 },
};
static const struct color bg_colors[] = {
- { "none", NULL },
- { "black", "40m" },
- { "red", "41m" },
- { "green", "42m" },
- { "yellow", "43m" },
- { "blue", "44m" },
- { "magenta", "45m" },
- { "cyan", "46m" },
- { "white", "47m" },
- { "default", "49m" },
+ { "none", NULL, 0 },
+ { "black", "40m", 1 },
+ { "red", "41m", 2 },
+ { "green", "42m", 3 },
+ { "yellow", "43m", 4 },
+ { "blue", "44m", 5 },
+ { "magenta", "45m", 6 },
+ { "cyan", "46m", 7 },
+ { "white", "47m", 8 },
+ { "default", "49m", 9 },
};
struct bytes_size {
FMT_FILE,
FMT_TYPE,
FMT_CONF,
- FMT_CONF_FILE
+ FMT_CONF_FILE,
+ FMT_RAINBOW
};
static const char *formats[] = {
"%s", /* generic */
"%s: %s", /* file */
"%s: %s: %s", /* type */
"%s: option '%s' %s", /* conf */
- "config file %s: %s" /* conf file */
+ "config file %s: %s", /* conf file */
+ "%s color '%s' %s" /* rainbow */
};
enum { GENERIC, FOREGROUND = 0, BACKGROUND };
enum opt_set {
OPT_ATTR_SET = 0x01,
OPT_EXCLUDE_RANDOM_SET = 0x02,
- OPT_OMIT_COLOR_EMPTY_SET = 0x04
+ OPT_OMIT_COLOR_EMPTY_SET = 0x04,
+ OPT_RAINBOW_FG_SET = 0x08
};
static struct {
char *attr;
OPT_CONFIG,
OPT_EXCLUDE_RANDOM,
OPT_OMIT_COLOR_EMPTY,
+ OPT_RAINBOW_FG,
OPT_HELP,
OPT_VERSION
};
{ "config", required_argument, &opt_type, OPT_CONFIG },
{ "exclude-random", required_argument, &opt_type, OPT_EXCLUDE_RANDOM },
{ "omit-color-empty", no_argument, &opt_type, OPT_OMIT_COLOR_EMPTY },
+ { "rainbow-fg", no_argument, &opt_type, OPT_RAINBOW_FG },
{ "help", no_argument, &opt_type, OPT_HELP },
{ "version", no_argument, &opt_type, OPT_VERSION },
{ NULL, 0, NULL, 0 },
static bool clean;
static bool clean_all;
static bool omit_color_empty;
+static bool rainbow_fg;
static char attr[MAX_ATTRIBUTE_CHARS + 1];
static char *exclude;
static void find_color_entries (struct color_name **, const struct color **);
static void find_color_entry (const struct color_name *, unsigned int, const struct color **);
static void print_line (const char *, const struct color **, const char * const, unsigned int, bool);
+static unsigned int get_rainbow_index (const struct color **, unsigned int, unsigned int);
+static bool skipable_rainbow_index (const struct color **, unsigned int);
static void print_clean (const char *);
static bool is_esc (const char *);
static const char *get_end_of_esc (const char *);
const char *file = NULL;
char *conf_file = NULL;
- struct conf config = { NULL, NULL, NULL, NULL };
+ struct conf config = { NULL, NULL, NULL, NULL, NULL };
program_name = argv[0];
atexit (cleanup);
{ "attr", OPT_ATTR_SET },
{ "exclude-random", OPT_EXCLUDE_RANDOM_SET },
{ "omit-color-empty", OPT_OMIT_COLOR_EMPTY_SET },
+ { "rainbow-fg", OPT_RAINBOW_FG_SET },
};
for (i = 0; i < COUNT_OF (options, struct option_set); i++)
if (opts_set & options[i].set)
case OPT_OMIT_COLOR_EMPTY:
opts_set |= OPT_OMIT_COLOR_EMPTY_SET;
break;
+ case OPT_RAINBOW_FG:
+ opts_set |= OPT_RAINBOW_FG_SET;
+ break;
case OPT_HELP:
PRINT_HELP_EXIT ();
case OPT_VERSION:
process_opt_exclude_random (opts_arg.exclude_random, true);
if (opts_set & OPT_OMIT_COLOR_EMPTY_SET)
omit_color_empty = true;
+ if (opts_set & OPT_RAINBOW_FG_SET)
+ rainbow_fg = true;
RELEASE (opts_arg.attr);
RELEASE (opts_arg.exclude_random);
ASSIGN_CONF (config->exclude_random, val);
else if (streq (cfg, "omit-color-empty"))
ASSIGN_CONF (config->omit_color_empty, val);
+ else if (streq (cfg, "rainbow-fg"))
+ ASSIGN_CONF (config->rainbow_fg, val);
else
vfprintf_fail (formats[FMT_CONF], conf_file, cfg, "not recognized");
}
else
vfprintf_fail (formats[FMT_GENERIC], "omit-color-empty conf option is not valid");
}
+ if (config->rainbow_fg)
+ {
+ if (streq (config->rainbow_fg, "yes"))
+ rainbow_fg = true;
+ else if (streq (config->rainbow_fg, "no"))
+ rainbow_fg = false;
+ else
+ vfprintf_fail (formats[FMT_GENERIC], "rainbow-fg conf option is not valid");
+ }
}
static void
RELEASE (config->color);
RELEASE (config->exclude_random);
RELEASE (config->omit_color_empty);
+ RELEASE (config->rainbow_fg);
}
static void
}
}
+ /* --rainbow-fg */
+ if (rainbow_fg)
+ {
+ unsigned int i;
+ const unsigned int color_set[2] = { FOREGROUND, BACKGROUND };
+ for (i = 0; i < 2; i++)
+ {
+ const unsigned int color = color_set[i];
+ if (color_names[color] && (
+ streq (color_names[color]->name, "none")
+ || streq (color_names[color]->name, "default"))
+ )
+ vfprintf_fail (formats[FMT_RAINBOW], tables[color].desc, color_names[color]->orig, "cannot be used with --rainbow-fg");
+ }
+ }
+
find_color_entries (color_names, colors);
assert (colors[FOREGROUND] != NULL);
free_color_names (color_names);
/* skip for --omit-color-empty? */
else if (emit_colors)
{
+ /* --rainbow-fg */
+ if (rainbow_fg)
+ {
+ unsigned int index;
+ const unsigned int max_index = tables[FOREGROUND].count - 2; /* omit color default */
+
+ if (rainbow_index == 0)
+ rainbow_index = colors[FOREGROUND]->index; /* init */
+ else if (rainbow_index > max_index)
+ rainbow_index = 1; /* black */
+
+ index = get_rainbow_index (colors, rainbow_index, max_index);
+
+ colors[FOREGROUND] = (struct color *)&tables[FOREGROUND].entries[index];
+
+ rainbow_index = index + 1;
+ }
+
/* Foreground color code is guaranteed to be set when background color code is present. */
if (colors[BACKGROUND] && colors[BACKGROUND]->code)
printf ("\033[%s", colors[BACKGROUND]->code);
putchar ('\n');
}
+static unsigned int
+get_rainbow_index (const struct color **colors, unsigned int index, unsigned int max)
+{
+ if (skipable_rainbow_index (colors, index))
+ {
+ if (index + 1 > max)
+ {
+ if (skipable_rainbow_index (colors, 1))
+ return 2;
+ else
+ return 1;
+ }
+ else
+ return index + 1;
+ }
+ else
+ return index;
+}
+
+static bool
+skipable_rainbow_index (const struct color **colors, unsigned int index)
+{
+ return (colors[BACKGROUND] && index == colors[BACKGROUND]->index);
+}
+
static void
print_clean (const char *line)
{
use Symbol qw(gensym);
use Test::More;
-my $tests = 7;
+my $tests = 8;
my $run_program_fail = sub
{
[ 'attr=bold,bold', 'attr conf option has attribute \'bold\' twice or more' ],
[ 'exclude-random=random', 'exclude-random conf option must be provided a plain color' ],
[ 'omit-color-empty=unsure', 'omit-color-empty conf option is not valid' ],
+ [ 'rainbow-fg=unsure', 'rainbow-fg conf option is not valid' ],
);
foreach my $set (@set) {
open(my $fh, '>', $conf_file) or die "Cannot open `$conf_file' for writing: $!\n";
attr=bold
color=blue
omit-color-empty=yes
+rainbow-fg=yes
EOT
my $expected = <<"EOT";
\e[1;34mfoo\e[0m
-\e[1;34mbar\e[0m
+\e[1;35mbar\e[0m
-\e[1;34mbaz\e[0m
+\e[1;36mbaz\e[0m
EOT
plan tests => $tests;
use Symbol qw(gensym);
use Test::More;
-my $tests = 9;
+my $tests = 10;
my $run_program_fail = sub
{
[ 'color1=magenta', 'option \'color1\' not recognized' ],
[ 'exclude-random1=black', 'option \'exclude-random1\' not recognized' ],
[ 'omit-color-empty1=yes', 'option \'omit-color-empty1\' not recognized' ],
+ [ 'rainbow-fg1=no', 'option \'rainbow-fg1\' not recognized' ],
[ 'attr', 'option \'attr\' not followed by =' ],
[ 'attr#', 'option \'attr\' not followed by =' ],
[ 'attr bold', 'option \'attr\' not followed by =' ],
use Symbol qw(gensym);
use Test::More;
-my $tests = 21;
+my $tests = 23;
my $conf = <<'EOT';
# comment
color=green
exclude-random=black
omit-color-empty=yes
+rainbow-fg=no
attr=bold # comment
attr=bold # comment
attr=
color=
exclude-random=
omit-color-empty=
+rainbow-fg=
EOT
my $run_program_succeed = sub
attr=underscore
color=yellow # tested also in color.t
omit-color-empty=yes
+rainbow-fg=yes
EOT
plan tests => $tests;
is(qx($program $infile1), <<"EOT", 'use config');
\e[4;33mfoo\e[0m
-\e[4;33mbar\e[0m
+\e[4;34mbar\e[0m
-\e[4;33mbaz\e[0m
+\e[4;35mbaz\e[0m
EOT
my $infile2 = $write_to_tmpfile->('foo');
use Symbol qw(gensym);
use Test::More;
-my $tests = 25;
+my $tests = 29;
my $run_program_fail = sub
{
my $dir = tempdir(CLEANUP => true);
my @set = (
- [ '--attr=:', 'must be provided a string' ],
- [ '--attr=bold:underscore', 'must have strings separated by ,' ],
- [ '--attr=b0ld', 'attribute \'b0ld\' is not valid' ],
- [ '--attr=b0ld,underscore', 'attribute \'b0ld\' is not valid' ], # handle comma
- [ '--attr=bold,bold', 'has attribute \'bold\' twice or more' ],
- [ '--exclude-random=random', 'must be provided a plain color' ],
- [ '--clean --clean-all', 'mutually exclusive' ],
- [ '--clean file1 file2', 'more than one file' ],
- [ '--clean-all file1 file2', 'more than one file' ],
- [ '- file', 'hyphen cannot be used as color string' ],
- [ '-', 'hyphen must be preceded by color string' ],
- [ "$file file", 'cannot be used as color string' ],
- [ "$file", 'must be preceded by color string' ],
- [ "$dir", 'is not a valid file type' ],
- [ '/black', 'foreground color missing' ],
- [ 'white/', 'background color missing' ],
- [ 'white/black/yellow', 'one color pair allowed only' ],
- [ 'y3llow', 'cannot be made of non-alphabetic characters' ],
- [ 'yEllow', 'cannot be in mixed lower/upper case' ],
- [ 'None', 'cannot be bold' ],
- [ 'white/Black', 'cannot be bold' ],
- [ 'random/none', 'cannot be combined with' ],
- [ 'random/default', 'cannot be combined with' ],
- [ 'none/random', 'cannot be combined with' ],
- [ 'default/random', 'cannot be combined with' ],
+ [ '--attr=:', 'must be provided a string' ],
+ [ '--attr=bold:underscore', 'must have strings separated by ,' ],
+ [ '--attr=b0ld', 'attribute \'b0ld\' is not valid' ],
+ [ '--attr=b0ld,underscore', 'attribute \'b0ld\' is not valid' ], # handle comma
+ [ '--attr=bold,bold', 'has attribute \'bold\' twice or more' ],
+ [ '--exclude-random=random', 'must be provided a plain color' ],
+ [ '--clean --clean-all', 'mutually exclusive' ],
+ [ '--clean file1 file2', 'more than one file' ],
+ [ '--clean-all file1 file2', 'more than one file' ],
+ [ '- file', 'hyphen cannot be used as color string' ],
+ [ '-', 'hyphen must be preceded by color string' ],
+ [ "$file file", 'cannot be used as color string' ],
+ [ "$file", 'must be preceded by color string' ],
+ [ "$dir", 'is not a valid file type' ],
+ [ '/black', 'foreground color missing' ],
+ [ 'white/', 'background color missing' ],
+ [ 'white/black/yellow', 'one color pair allowed only' ],
+ [ 'y3llow', 'cannot be made of non-alphabetic characters' ],
+ [ 'yEllow', 'cannot be in mixed lower/upper case' ],
+ [ 'None', 'cannot be bold' ],
+ [ 'white/Black', 'cannot be bold' ],
+ [ 'random/none', 'cannot be combined with' ],
+ [ 'random/default', 'cannot be combined with' ],
+ [ 'none/random', 'cannot be combined with' ],
+ [ 'default/random', 'cannot be combined with' ],
+ [ 'white/none --rainbow-fg', 'cannot be used with --rainbow-fg' ],
+ [ 'white/default --rainbow-fg', 'cannot be used with --rainbow-fg' ],
+ [ 'none/white --rainbow-fg', 'cannot be used with --rainbow-fg' ],
+ [ 'default/white --rainbow-fg', 'cannot be used with --rainbow-fg' ],
);
foreach my $set (@set) {
ok($run_program_fail->($program, $set->[0], $set->[1]), $set->[1]);
use Test::Harness qw(runtests);
use Test::More;
-my $tests = 32;
+my $tests = 34;
my $valgrind_cmd = '';
{
{
my $ok = true;
- foreach my $option (qw(--attr=bold --exclude-random=black --omit-color-empty)) {
+ foreach my $option (qw(--attr=bold --exclude-random=black --omit-color-empty --rainbow-fg)) {
$ok &= qx($valgrind_cmd$program $option $switch $infile1 2>&1 >/dev/null) =~ /switch has no meaning with/;
}
ok($ok, "$type strict options");
'switch omit-color-empty');
}
+ {
+ my $infile = $write_to_tmpfile->("foo\nbar\nbaz");
+
+ is_deeply([split /\n/, qx($valgrind_cmd$program black/black --rainbow-fg $infile)],
+ [split /\n/, "\e[40m\e[31mfoo\e[0m\n\e[40m\e[32mbar\e[0m\n\e[40m\e[33mbaz\e[0m"],
+ 'switch rainbow-fg (init)');
+
+ is_deeply([split /\n/, qx($valgrind_cmd$program white --rainbow-fg $infile)],
+ [split /\n/, "\e[37mfoo\e[0m\n\e[30mbar\e[0m\n\e[31mbaz\e[0m"],
+ 'switch rainbow-fg (reset)');
+ }
+
SKIP: {
skip 'valgrind not found', 1 unless system('which valgrind >/dev/null 2>&1') == 0;
like(qx(valgrind $program none/none $infile1 2>&1 >/dev/null), qr/no leaks are possible/, 'valgrind memleaks');