diff options
Diffstat (limited to 'filters/colorize.c')
-rw-r--r-- | filters/colorize.c | 681 |
1 files changed, 681 insertions, 0 deletions
diff --git a/filters/colorize.c b/filters/colorize.c new file mode 100644 index 00000000..17eb548a --- /dev/null +++ b/filters/colorize.c @@ -0,0 +1,681 @@ +/* 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 }, + .header = { .bold = 1 }, + .signature = { .dim = 1 }, + .diff_meta = { .bold = 1 }, + .diff_chunk = { .dim = 1 }, + .diff_add = { .fg = { .type = PALETTE, .index = 2 } }, + .diff_del = { .fg = { .type = PALETTE, .index = 1 } }, + .quote_1 = { .fg = { .type = PALETTE, .index = 6 } }, + .quote_2 = { .fg = { .type = PALETTE, .index = 6 }, .dim = 1 }, + .quote_3 = { .fg = { .type = PALETTE, .index = 6 }, .dim = 1 }, + .quote_4 = { .fg = { .type = PALETTE, .index = 6 }, .dim = 1 }, + .quote_x = { .fg = { .type = PALETTE, .index = 6 }, .dim = 1 }, +}; + +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) + 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}://[[:graph:]]{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 '?': 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; +} |