/* SPDX-License-Identifier: MIT */
/* Copyright (c) 2023 Robin Jarry */
#include <errno.h>
#include <fnmatch.h>
#include <getopt.h>
#include <regex.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static void usage(void)
{
puts("usage: colorize [-h] [-s FILE] [-f FILE]");
puts("");
puts("options:");
puts(" -h show this help message");
puts(" -s FILE use styleset file (default $AERC_STYLESET)");
puts(" -f FILE read from filename (default stdin)");
}
enum color_type {
NONE = 0,
DEFAULT,
RGB,
PALETTE,
};
struct color {
enum color_type type;
uint32_t rgb;
uint32_t index;
};
struct style {
struct color fg;
struct color bg;
int bold;
int blink;
int underline;
int reverse;
int italic;
int dim;
char *sequence;
};
__attribute__((malloc,returns_nonnull))
static void *xmalloc(size_t s)
{
void *ptr = malloc(s);
if (ptr == NULL) {
perror("fatal: cannot allocate buffer");
abort();
}
return ptr;
}
#define BOLD "\x1b[1m"
#define RESET "\x1b[0m"
#define LONGEST_SEQ "\x1b[1;2;3;4;5;7;38;2;255;255;255;48;2;255;255;255m"
const char *seq(struct style *s) {
if (!s->sequence) {
char *b, *buf = xmalloc(strlen(LONGEST_SEQ) + 1);
const char *sep = "";
b = buf;
b += sprintf(b, "%s", "\x1b[");
if (s->bold) {
b += sprintf(b, "%s1", sep);
sep = ";";
}
if (s->dim) {
b += sprintf(b, "%s2", sep);
sep = ";";
}
if (s->italic) {
b += sprintf(b, "%s3", sep);
sep = ";";
}
if (s->underline) {
b += sprintf(b, "%s4", sep);
sep = ";";
}
if (s->blink) {
b += sprintf(b, "%s5", sep);
sep = ";";
}
if (s->reverse) {
b += sprintf(b, "%s7", sep);
sep = ";";
}
switch (s->fg.type) {
case NONE:
break;
case DEFAULT:
b += sprintf(b, "%s39", sep);
break;
case RGB:
b += sprintf(b, "%s38;2;%d;%d;%d", sep,
(s->fg.rgb >> 16) & 0xff,
(s->fg.rgb >> 8) & 0xff,
s->fg.rgb & 0xff);
sep = ";";
break;
case PALETTE:
b += sprintf(b, (s->fg.index < 8) ?
"%s3%d" : "%s38;5;%d", sep, s->fg.index);
sep = ";";
break;
}
switch (s->bg.type) {
case NONE:
break;
case DEFAULT:
b += sprintf(b, "%s49", sep);
break;
case RGB:
b += sprintf(b, "%s48;2;%d;%d;%d", sep,
(s->bg.rgb >> 16) & 0xff,
(s->bg.rgb >> 8) & 0xff,
s->bg.rgb & 0xff);
break;
case PALETTE:
b += sprintf(b, (s->bg.index < 8) ?
"%s4%d" : "%s48;5;%d", sep, s->bg.index);
break;
}
if (strcmp(buf, "\x1b[") == 0) {
b += sprintf(b, "0");
}
sprintf(b, "m");
s->sequence = buf;
}
return s->sequence;
}
struct styles {
struct style url;
struct style header;
struct style signature;
struct style diff_meta;
struct style diff_chunk;
struct style diff_add;
struct style diff_del;
struct style quote_1;
struct style quote_2;
struct style quote_3;
struct style quote_4;
struct style quote_x;
};
static FILE *in_file;
static const char *styleset;
static struct styles styles = {
.url = { .underline = 1, .fg = { .type = RGB, .rgb = 0xffffaf } },
.header = { .bold = 1, .fg = { .type = RGB, .rgb = 0xaf87ff } },
.signature = { .dim = 1, .fg = { .type = RGB, .rgb = 0xaf87ff } },
.diff_meta = { .bold = 1, .fg = { .type = RGB, .rgb = 0xffffff } },
.diff_chunk = { .fg = { .type = RGB, .rgb = 0x00cdcd } },
.diff_add = { .fg = { .type = RGB, .rgb = 0x00cd00 } },
.diff_del = { .fg = { .type = RGB, .rgb = 0xcd0000 } },
.quote_1 = { .fg = { .type = RGB, .rgb = 0x5fafff } },
.quote_2 = { .fg = { .type = RGB, .rgb = 0xff8700 } },
.quote_3 = { .fg = { .type = RGB, .rgb = 0xaf87ff } },
.quote_4 = { .fg = { .type = RGB, .rgb = 0xff5fd7 } },
.quote_x = { .fg = { .type = RGB, .rgb = 0x808080 } },
};
static inline int startswith(const char *s, const char *prefix)
{
return !strncmp(s, prefix, strlen(prefix));
}
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
static struct { const char *n; uint32_t c; } color_names[] = {
{"aliceblue", 0xf0f8ff}, {"antiquewhite", 0xfaebd7}, {"aqua", 0x00ffff},
{"aquamarine", 0x7fffd4}, {"azure", 0xf0ffff}, {"beige", 0xf5f5dc},
{"bisque", 0xffe4c4}, {"black", 0x000000}, {"blanchedalmond", 0xffebcd},
{"blue", 0x0000ff}, {"blueviolet", 0x8a2be2}, {"brown", 0xa52a2a},
{"burlywood", 0xdeb887}, {"cadetblue", 0x5f9ea0}, {"chartreuse", 0x7fff00},
{"chocolate", 0xd2691e}, {"coral", 0xff7f50}, {"cornflowerblue", 0x6495ed},
{"cornsilk", 0xfff8dc}, {"crimson", 0xdc143c}, {"darkblue", 0x00008b},
{"darkcyan", 0x008b8b}, {"darkgoldenrod", 0xb8860b}, {"darkgray", 0xa9a9a9},
{"darkgreen", 0x006400}, {"darkkhaki", 0xbdb76b}, {"darkmagenta", 0x8b008b},
{"darkolivegreen", 0x556b2f}, {"darkorange", 0xff8c00}, {"darkorchid", 0x9932cc},
{"darkred", 0x8b0000}, {"darksalmon", 0xe9967a}, {"darkseagreen", 0x8fbc8f},
{"darkslateblue", 0x483d8b}, {"darkslategray", 0x2f4f4f}, {"darkturquoise", 0x00ced1},
{"darkviolet", 0x9400d3}, {"deeppink", 0xff1493}, {"deepskyblue", 0x00bfff},
{"dimgray", 0x696969}, {"dodgerblue", 0x1e90ff}, {"firebrick", 0xb22222},
{"floralwhite", 0xfffaf0}, {"forestgreen", 0x228b22}, {"fuchsia", 0xff00ff},
{"gainsboro", 0xdcdcdc}, {"ghostwhite", 0xf8f8ff}, {"gold", 0xffd700},
{"goldenrod", 0xdaa520}, {"gray", 0x808080}, {"green", 0x008000},
{"greenyellow", 0xadff2f}, {"honeydew", 0xf0fff0}, {"hotpink", 0xff69b4},
{"indianred", 0xcd5c5c}, {"indigo", 0x4b0082}, {"ivory", 0xfffff0},
{"khaki", 0xf0e68c}, {"lavender", 0xe6e6fa}, {"lavenderblush", 0xfff0f5},
{"lawngreen", 0x7cfc00}, {"lemonchiffon", 0xfffacd}, {"lightblue", 0xadd8e6},
{"lightcoral", 0xf08080}, {"lightcyan", 0xe0ffff}, {"lightgoldenrodyellow", 0xfafad2},
{"lightgray", 0xd3d3d3}, {"lightgreen", 0x90ee90}, {"lightpink", 0xffb6c1},
{"lightsalmon", 0xffa07a}, {"lightseagreen", 0x20b2aa}, {"lightskyblue", 0x87cefa},
{"lightslategray", 0x778899}, {"lightsteelblue", 0xb0c4de}, {"lightyellow", 0xffffe0},
{"lime", 0x00ff00}, {"limegreen", 0x32cd32}, {"linen", 0xfaf0e6},
{"maroon", 0x800000}, {"mediumaquamarine", 0x66cdaa}, {"mediumblue", 0x0000cd},
{"mediumorchid", 0xba55d3}, {"mediumpurple", 0x9370db}, {"mediumseagreen", 0x3cb371},
{"mediumslateblue", 0x7b68ee}, {"mediumspringgreen", 0x00fa9a}, {"mediumturquoise", 0x48d1cc},
{"mediumvioletred", 0xc71585}, {"midnightblue", 0x191970}, {"mintcream", 0xf5fffa},
{"mistyrose", 0xffe4e1}, {"moccasin", 0xffe4b5}, {"navajowhite", 0xffdead},
{"navy", 0x000080}, {"oldlace", 0xfdf5e6}, {"olive", 0x808000},
{"olivedrab", 0x6b8e23}, {"orange", 0xffa500}, {"orangered", 0xff4500},
{"orchid", 0xda70d6}, {"palegoldenrod", 0xeee8aa}, {"palegreen", 0x98fb98},
{"paleturquoise", 0xafeeee}, {"palevioletred", 0xdb7093}, {"papayawhip", 0xffefd5},
{"peachpuff", 0xffdab9}, {"peru", 0xcd853f}, {"pink", 0xffc0cb},
{"plum", 0xdda0dd}, {"powderblue", 0xb0e0e6}, {"purple", 0x800080},
{"rebeccapurple", 0x663399}, {"red", 0xff0000}, {"rosybrown", 0xbc8f8f},
{"royalblue", 0x4169e1}, {"saddlebrown", 0x8b4513}, {"salmon", 0xfa8072},
{"sandybrown", 0xf4a460}, {"seagreen", 0x2e8b57}, {"seashell", 0xfff5ee},
{"sienna", 0xa0522d}, {"silver", 0xc0c0c0}, {"skyblue", 0x87ceeb},
{"slateblue", 0x6a5acd}, {"slategray", 0x708090}, {"snow", 0xfffafa},
{"springgreen", 0x00ff7f}, {"steelblue", 0x4682b4}, {"tan", 0xd2b48c},
{"teal", 0x008080}, {"thistle", 0xd8bfd8}, {"tomato", 0xff6347},
{"turquoise", 0x40e0d0}, {"violet", 0xee82ee}, {"wheat", 0xf5deb3},
{"white", 0xffffff}, {"whitesmoke", 0xf5f5f5}, {"yellow", 0xffff00},
{"yellowgreen", 0x9acd32},
};
static int color_name(const char *name, uint32_t *color)
{
for (size_t c = 0; c < ARRAY_SIZE(color_names); c++) {
if (!strcmp(name, color_names[c].n)) {
*color = color_names[c].c;
return 0;
}
}
return 1;
}
static int parse_color(struct color *c, const char *val)
{
uint32_t color = 0;
if (!strcmp(val, "default")) {
c->type = DEFAULT;
} else if (sscanf(val, "#%x", &color) == 1 && color <= 0xffffff) {
c->type = RGB;
c->rgb = color;
} else if (sscanf(val, "%d", &color) == 1 && color <= 256) {
c->type = PALETTE;
c->index = color;
} else if (!color_name(val, &color)) {
c->type = RGB;
c->rgb = color;
} else {
fprintf(stderr, "error: invalid color value %s\n", val);
return 1;
}
return 0;
}
static int parse_bool(int *b, const char *val)
{
if (!strcmp(val, "true")) {
*b = 1;
} else if (!strcmp(val, "false")) {
*b = 0;
} else if (!strcmp(val, "toggle")) {
*b = !*b;
} else {
fprintf(stderr, "error: invalid bool value %s\n", val);
return 1;
}
return 0;
}
static int set_attr(struct style *s, const char *attr, const char *val)
{
if (!strcmp(attr, "fg")) {
if (parse_color(&s->fg, val))
return 1;
} else if (!strcmp(attr, "bg")) {
if (parse_color(&s->fg, val))
return 1;
} else if (!strcmp(attr, "bold")) {
if (parse_bool(&s->bold, val))
return 1;
} else if (!strcmp(attr, "blink")) {
if (parse_bool(&s->blink, val))
return 1;
} else if (!strcmp(attr, "underline")) {
if (parse_bool(&s->underline, val))
return 1;
} else if (!strcmp(attr, "reverse")) {
if (parse_bool(&s->reverse, val))
return 1;
} else if (!strcmp(attr, "italic")) {
if (parse_bool(&s->italic, val))
return 1;
} else if (!strcmp(attr, "dim")) {
if (parse_bool(&s->dim, val))
return 1;
} else if (!strcmp(attr, "normal")) {
s->bold = 0;
s->underline = 0;
s->reverse = 0;
s->italic = 0;
s->dim = 0;
} else if (!strcmp(attr, "default")) {
s->fg.type = NONE;
s->fg.type = NONE;
} else {
fprintf(stderr, "error: invalid style attribute %s\n", attr);
return 1;
}
return 0;
}
static struct {const char *n; struct style *s;} ini_objects[] = {
{"url", &styles.url},
{"header", &styles.header},
{"signature", &styles.signature},
{"diff_meta", &styles.diff_meta},
{"diff_chunk", &styles.diff_chunk},
{"diff_add", &styles.diff_add},
{"diff_del", &styles.diff_del},
{"quote_1", &styles.quote_1},
{"quote_2", &styles.quote_2},
{"quote_3", &styles.quote_3},
{"quote_4", &styles.quote_4},
{"quote_x", &styles.quote_x},
};
static int parse_styleset(void)
{
int in_section = 0;
char buf[BUFSIZ];
int err = 0;
FILE *f;
if (!styleset)
return 0;
f = fopen(styleset, "r");
if (!f) {
perror("error: failed to open styleset");
return 1;
}
while (fgets(buf, sizeof(buf), f)) {
/* strip LF, CR, CRLF, LFCR */
buf[strcspn(buf, "\r\n")] = '\0';
if (in_section) {
char obj[64], attr[64], val[64];
int changed = 0;
if (sscanf(buf, "%63[^.].%63[^=] = %63s", obj, attr, val) != 3) {
if (buf[0] == '[') {
/* start of another section */
break;
}
continue;
}
for (size_t o = 0; o < ARRAY_SIZE(ini_objects); o++) {
if (fnmatch(obj, ini_objects[o].n, 0))
continue;
if (set_attr(ini_objects[o].s, attr, val)) {
err = 1;
goto end;
}
changed++;
}
if (!changed) {
fprintf(stderr,
"error: unknown style object %s\n",
obj);
err = 1;
goto end;
}
} else if (!strcmp(buf, "[viewer]")) {
in_section = 1;
/* only disable the default theme if there is
* a [viewer] section in the styleset */
memset(&styles, 0, sizeof(styles));
}
}
end:
fclose(f);
return err;
}
static inline void print(const char *in)
{
fputs(in, stdout);
}
static inline size_t print_notabs(const char *in, size_t max_len)
{
size_t len = 0;
while (*in != '\0' && len < max_len) {
char c = *in++;
if (c == '\t') {
/* Tabs are interpreted as cursor movement and are not
* colored like regular characters. Replace them with
* 8 spaces. */
fputs(" ", stdout);
} else {
fputc(c, stdout);
}
len++;
}
return len;
}
static void diff_chunk(const char *in)
{
size_t len = 0;
print(seq(&styles.diff_chunk));
while (in[len] == '@')
len++;
while (in[len] != '\0' && in[len] != '@')
len++;
while (in[len] == '@')
len++;
in += print_notabs(in, len);
print(RESET);
print_notabs(in, BUFSIZ);
}
#define URL_RE \
"[a-z]{2,8}://[][:alnum:]._~:/?#[@!$&'()*+,;=%-]{4,}" \
"|(mailto:)?[[:alnum:]_+.~/-]*[[:alnum:]]@[a-z][[:alnum:].-]*[a-z]"
static regex_t url_re;
static void urls(const char *in, struct style *ctx)
{
regmatch_t groups[2];
size_t len;
int trim;
while (!regexec(&url_re, in, 2, groups, 0)) {
in += print_notabs(in, groups[0].rm_so);
print(seq(&styles.url));
len = groups[0].rm_eo - groups[0].rm_so;
/* Heuristic to remove trailing characters that are valid URL
* characters, but typically not at the end of the URL */
trim = 1;
while (trim && len > 0) {
switch (in[len - 1]) {
case '.': case ',': case ';': case ')':
case '!': case '?': case '\'':
len--;
break;
default:
trim = 0;
break;
}
}
in += print_notabs(in, len);
print(RESET);
if (ctx) {
print(seq(ctx));
}
}
print_notabs(in, BUFSIZ);
}
static inline void signature(const char *in)
{
print(seq(&styles.signature));
urls(in, &styles.signature);
print(RESET);
}
#define HEADER_RE "^[A-Z][[:alnum:]_-]+:"
static regex_t header_re;
static void header(const char *in)
{
regmatch_t groups[1];
if (!regexec(&header_re, in, 1, groups, 0)) {
print(seq(&styles.header));
in += print_notabs(in, groups[0].rm_eo);
print(RESET);
}
urls(in, NULL);
}
#define DIFF_META_RE \
"^(diff --git|(new|deleted) file|similarity" \
" index|(rename|copy) (to|from)|index|---|\\+\\+\\+) "
static regex_t diff_meta_re;
static void quote(const char *in)
{
regmatch_t groups[8];
struct style *s;
int q, level;
q = level = 0;
while (in[q] == '>') {
level++;
q++;
if (in[q] == ' ')
q++;
}
switch (level) {
case 1:
s = &styles.quote_1;
break;
case 2:
s = &styles.quote_2;
break;
case 3:
s = &styles.quote_3;
break;
case 4:
s = &styles.quote_4;
break;
default:
s = &styles.quote_x;
break;
}
print(seq(s));
in += print_notabs(in, q);
if (startswith(in, "+")) {
printf("%s%s", RESET, seq(&styles.diff_add));
print_notabs(in, BUFSIZ);
} else if (startswith(in, "-")) {
printf("%s%s", RESET, seq(&styles.diff_del));
print_notabs(in, BUFSIZ);
} else if (!regexec(&diff_meta_re, in, 8, groups, 0)) {
print(BOLD);
print_notabs(in, BUFSIZ);
} else {
urls(in, s);
}
print(RESET);
}
static void print_style(const char *in, struct style *s)
{
print(seq(s));
print_notabs(in, BUFSIZ);
print(RESET);
}
enum state { INIT, DIFF, SIGNATURE, BODY };
static void colorize_line(const char *in)
{
static enum state state = INIT;
regmatch_t groups[8]; /* enough groups to cover all expressions */
switch (state) {
case DIFF:
if (!strcmp(in, "--") || !strcmp(in, "-- ")) {
state = SIGNATURE;
signature(in);
} else if (startswith(in, "@@ ")) {
diff_chunk(in);
} else if (!regexec(&diff_meta_re, in, 8, groups, 0)) {
print_style(in, &styles.diff_meta);
} else if (startswith(in, "+")) {
print_style(in, &styles.diff_add);
} else if (startswith(in, "-")) {
print_style(in, &styles.diff_del);
} else if (!startswith(in, " ") && strcmp(in, "") != 0) {
state = BODY;
if (startswith(in, ">")) {
quote(in);
} else {
urls(in, NULL);
}
} else {
print_notabs(in, BUFSIZ);
}
break;
case SIGNATURE:
signature(in);
break;
case BODY:
if (startswith(in, ">")) {
quote(in);
} else if (startswith(in, "diff --git ")) {
state = DIFF;
print_style(in, &styles.diff_meta);
} else if (!strcmp(in, "--") || !strcmp(in, "-- ")) {
state = SIGNATURE;
signature(in);
} else if (!regexec(&header_re, in, 8, groups, 0)) {
header(in);
} else {
urls(in, NULL);
}
break;
default: /* INIT */
if (startswith(in, "diff --git ")) {
state = DIFF;
print_style(in, &styles.diff_meta);
} else if (!strcmp(in, "--") || !strcmp(in, "-- ")) {
state = SIGNATURE;
signature(in);
} else {
state = BODY;
if (startswith(in, ">")) {
quote(in);
} else if (!regexec(&header_re, in, 8, groups, 0)) {
header(in);
} else {
urls(in, NULL);
}
}
break;
}
}
int parse_args(int argc, char **argv)
{
const char *filename = NULL;
char c;
styleset = getenv("AERC_STYLESET");
while ((c = getopt(argc, argv, "hs:f:")) != -1) {
switch (c) {
case 's':
styleset = optarg;
break;
case 'f':
filename = optarg;
break;
default:
usage();
return 1;
}
}
if (optind < argc) {
fprintf(stderr, "%s: unexpected argument -- '%s'\n",
argv[0], argv[optind]);
usage();
return 1;
}
if (filename == NULL || !strcmp(filename, "-")) {
in_file = stdin;
} else {
in_file = fopen(filename, "r");
if (!in_file) {
perror("error: cannot open file");
return 1;
}
}
return 0;
}
int main(int argc, char **argv)
{
char buf[BUFSIZ];
int err;
regcomp(&header_re, HEADER_RE, REG_EXTENDED);
regcomp(&diff_meta_re, DIFF_META_RE, REG_EXTENDED);
regcomp(&url_re, URL_RE, REG_EXTENDED);
err = parse_args(argc, argv);
if (err) {
goto end;
}
err = parse_styleset();
if (err) {
goto end;
}
while (fgets(buf, sizeof(buf), in_file)) {
/* strip LF, CR, CRLF, LFCR */
buf[strcspn(buf, "\r\n")] = '\0';
colorize_line(buf);
printf("\n");
}
end:
if (in_file) {
fclose(in_file);
}
return err;
}