]> git.refcnt.org Git - colorize.git/commitdiff
Add initial implementation of foreground color rainbow mode
authorSteven Schubiger <stsc@refcnt.org>
Thu, 25 Jul 2024 12:35:26 +0000 (14:35 +0200)
committerSteven Schubiger <stsc@refcnt.org>
Thu, 25 Jul 2024 12:35:26 +0000 (14:35 +0200)
colorize.1
colorize.c
t/conf/fail.t
t/conf/param.t
t/conf/parse/fail.t
t/conf/parse/success.t
t/conf/use.t
t/fail.t
test.pl

index afa1f870c6f17041625285d702dfa68f0a039005..0f805fe68c184668b1d4fc98f1463f0bbcfa243c 100644 (file)
@@ -1,4 +1,4 @@
-.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
@@ -49,6 +49,9 @@ text color to be excluded when selecting a random foreground color
 .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
@@ -76,6 +79,7 @@ attr = bold,underscore
 color = magenta # favorite one
 exclude-random = black
 omit-color-empty = yes
+rainbow-fg = no
 .fi
 .RE
 .RE
@@ -88,6 +92,7 @@ attr             (values same as command-line option)
 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
index 157c4f0cc80d763249e8ac6a5a7ab79d3aad7023..9635471734fa2a8f687780d02cde558f72322442 100644 (file)
@@ -141,6 +141,7 @@ struct conf {
     char *color;
     char *exclude_random;
     char *omit_color_empty;
+    char *rainbow_fg;
 };
 
 enum { DESC_OPTION, DESC_CONF };
@@ -153,31 +154,34 @@ struct color_name {
 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 {
@@ -195,7 +199,8 @@ enum {
     FMT_FILE,
     FMT_TYPE,
     FMT_CONF,
-    FMT_CONF_FILE
+    FMT_CONF_FILE,
+    FMT_RAINBOW
 };
 static const char *formats[] = {
     "%s",                     /* generic   */
@@ -207,7 +212,8 @@ static const char *formats[] = {
     "%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 };
@@ -225,7 +231,8 @@ static unsigned int opts_set;
 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;
@@ -239,6 +246,7 @@ enum {
     OPT_CONFIG,
     OPT_EXCLUDE_RANDOM,
     OPT_OMIT_COLOR_EMPTY,
+    OPT_RAINBOW_FG,
     OPT_HELP,
     OPT_VERSION
 };
@@ -250,6 +258,7 @@ static const struct option long_opts[] = {
     { "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                    },
@@ -289,6 +298,7 @@ static struct var_list *vars_list;
 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;
@@ -325,6 +335,8 @@ static void save_char (char, char **, size_t *, size_t *);
 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 *);
@@ -372,7 +384,7 @@ main (int argc, char **argv)
     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);
@@ -443,6 +455,7 @@ main (int argc, char **argv)
               { "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)
@@ -543,6 +556,9 @@ process_opts (int argc, char **argv, char **conf_file)
                   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:
@@ -694,6 +710,8 @@ init_opts_vars (void)
       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);
@@ -800,6 +818,8 @@ assign_conf (const char *conf_file, struct conf *config, const char *cfg, char *
       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");
 }
@@ -820,6 +840,15 @@ init_conf_vars (const struct conf *config)
         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
@@ -998,6 +1027,7 @@ free_conf (struct conf *config)
     RELEASE (config->color);
     RELEASE (config->exclude_random);
     RELEASE (config->omit_color_empty);
+    RELEASE (config->rainbow_fg);
 }
 
 static void
@@ -1071,6 +1101,22 @@ process_args (unsigned int arg_cnt, char **arg_strings, char *attr, const struct
           }
       }
 
+    /* --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);
@@ -1507,6 +1553,24 @@ print_line (const char *attr, const struct color **colors, const char *const lin
     /* 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);
@@ -1521,6 +1585,31 @@ print_line (const char *attr, const struct color **colors, const char *const lin
       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)
 {
index d3b88521ea1ca69c8375993a6e400a2d015e3695..9e16a495b3813cece19b66ab89d040dcf614a058 100755 (executable)
@@ -12,7 +12,7 @@ use IPC::Open3 qw(open3);
 use Symbol qw(gensym);
 use Test::More;
 
-my $tests = 7;
+my $tests = 8;
 
 my $run_program_fail = sub
 {
@@ -46,6 +46,7 @@ SKIP: {
         [ '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";
index 520df2be5fb71991bb2f11dac1b645716772be73..f8f535f91b8e046e93bac33b1fae60b2f397d372 100755 (executable)
@@ -14,14 +14,15 @@ my $conf = <<'EOT';
 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;
index d2988e496f4be49161780292cd1295c525ec6c5a..13f27ff153614b69a182cbe2a96b5e78c82e1e37 100755 (executable)
@@ -12,7 +12,7 @@ use IPC::Open3 qw(open3);
 use Symbol qw(gensym);
 use Test::More;
 
-my $tests = 9;
+my $tests = 10;
 
 my $run_program_fail = sub
 {
@@ -46,6 +46,7 @@ SKIP: {
         [ '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 ='                         ],
index fe2d46de8a533229dd38ec231fe9350096dd73ad..1ce06b0fb5c5617f645082cccc2a9b65ba70d3f5 100755 (executable)
@@ -12,7 +12,7 @@ use IPC::Open3 qw(open3);
 use Symbol qw(gensym);
 use Test::More;
 
-my $tests = 21;
+my $tests = 23;
 
 my $conf = <<'EOT';
 # comment
@@ -30,12 +30,14 @@ color=green
 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
index 0a695dfa8c847fb5f099ae93a76c4e2aeebd4d91..5132afa39099910aeea8cd68fb99e9f9017b6ffb 100755 (executable)
@@ -14,6 +14,7 @@ my $conf = <<'EOT';
 attr=underscore
 color=yellow # tested also in color.t
 omit-color-empty=yes
+rainbow-fg=yes
 EOT
 
 plan tests => $tests;
@@ -38,9 +39,9 @@ EOT
     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');
 
index 10378c412548fc694ad1e711dbf3c983b19d82fd..c44cb7aacc79a198b93542156571148d44c191f5 100755 (executable)
--- a/t/fail.t
+++ b/t/fail.t
@@ -12,7 +12,7 @@ use IPC::Open3 qw(open3);
 use Symbol qw(gensym);
 use Test::More;
 
-my $tests = 25;
+my $tests = 29;
 
 my $run_program_fail = sub
 {
@@ -40,31 +40,35 @@ SKIP: {
     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]);
diff --git a/test.pl b/test.pl
index 928c6987054f644dd06e48baac2b384118a05c85..54677220daa5d41bad881039392a200b6de126ca 100755 (executable)
--- a/test.pl
+++ b/test.pl
@@ -13,7 +13,7 @@ use Getopt::Long qw(:config no_auto_abbrev no_ignore_case);
 use Test::Harness qw(runtests);
 use Test::More;
 
-my $tests = 32;
+my $tests = 34;
 
 my $valgrind_cmd = '';
 {
@@ -101,7 +101,7 @@ SKIP: {
 
         {
             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");
@@ -160,6 +160,18 @@ SKIP: {
                   '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');