aboutsummaryrefslogtreecommitdiffstats
path: root/filters
diff options
context:
space:
mode:
authorRobin Jarry <robin@jarry.cc>2023-01-15 23:27:29 +0100
committerRobin Jarry <robin@jarry.cc>2023-01-26 00:20:48 +0100
commit98e32d2ff22cba1e60caf6a252eee0d38f26f736 (patch)
tree249564b1942d0a42225243a8a598ec48bc896c6b /filters
parent3191ee171c435a43912264b131340af66fea8112 (diff)
downloadaerc-98e32d2ff22cba1e60caf6a252eee0d38f26f736.tar.gz
filters: rewrite colorize in c
Since its introduction, we had multiple issues with the colorize awk script with regard to non-GNU awk compatibility. Also, this script is standalone and the color theme must be hard coded into it. Reading from an external configuration file (aerc's styleset) from a non-GNU awk is close to impossible (and even far from trivial with GNU awk). Rewrite the builtin colorize filter in C to allow getting the color theme from aerc's active styleset. The theme is configured using the existing styleset syntax and attributes under a separate [viewer] section (see examples and man page). Export the active styleset file path to AERC_STYLESET env var when invoking the filter command so that colorize can access it and use it. I have tested compilation (with clang-analyzer and gcc -fanalyzer) and basic operation on FreeBSD, Fedora (glibc) and Alpine (muslibc). More tests would probably be required on MacOSX and older Linux distros. I also added test vectors to give some confidence that this works as expected. The execution with these vectors passed valgrind --leak-check=full without errors. NB: the default theme has changed to be more minimal. Sample stylesets have more colorful examples. The awk -v theme=xxx option is no longer supported. usage: colorize [-h] [-s FILE] [-f FILE] options: -h show this help message -s FILE use styleset file (default $AERC_STYLESET) -f FILE read from filename (default stdin) Signed-off-by: Robin Jarry <robin@jarry.cc> Tested-by: Bence Ferdinandy <bence@ferdinandy.com> Acked-by: Moritz Poldrack <moritz@poldrack.dev>
Diffstat (limited to 'filters')
-rwxr-xr-xfilters/colorize177
-rw-r--r--filters/colorize.c681
-rw-r--r--filters/vectors/colorize-patch.expected49
-rw-r--r--filters/vectors/colorize-patch.in49
-rw-r--r--filters/vectors/colorize-quotes.expected65
-rw-r--r--filters/vectors/colorize-quotes.in65
6 files changed, 909 insertions, 177 deletions
diff --git a/filters/colorize b/filters/colorize
deleted file mode 100755
index 9d2a7363..00000000
--- a/filters/colorize
+++ /dev/null
@@ -1,177 +0,0 @@
-#!/usr/bin/awk -f
-# Copyright (c) 2022 Robin Jarry
-#
-# A filter for the aerc mail program to colorize messages / attachments.
-# Basic colour themes are supported. To use a theme set the theme variable
-# in your aerc.conf accordingly, for example:
-#
-# text/plain=colorize -v theme=solarized
-
-BEGIN {
- if (theme == "solarized") {
- # R;G;B colors
- url = "\033[38;2;181;137;0m" # yellow
- header = "\033[38;2;211;54;130m" # magenta
- signature = "\033[38;2;211;54;130m" # magenta
- diff_meta = "\033[1;38;2;131;148;150m" # bold brblue
- diff_chunk = "\033[38;2;42;161;152m" # cyan
- diff_add = "\033[38;2;133;153;0m" # green
- diff_del = "\033[38;2;220;50;47m" # red
- quote_1 = "\033[38;2;38;139;210m" # blue
- quote_2 = "\033[38;2;203;75;22m" # brred
- quote_3 = "\033[38;2;211;54;130m" # magenta
- quote_4 = "\033[38;2;108;113;196m" # brmagenta
- quote_x = "\033[38;2;147;161;161m" # brcyan
- bold = "\033[1m"
- reset = "\033[0m"
- } else if (theme == "" || theme == "default") {
- # R;G;B colors
- url = "\033[38;2;255;255;175m" # yellow
- header = "\033[38;2;175;135;255m" # purple
- signature = "\033[38;2;175;135;255m" # purple
- diff_meta = "\033[1;38;2;255;255;255m" # bold white
- diff_chunk = "\033[38;2;0;205;205m" # cyan
- diff_add = "\033[38;2;0;205;0m" # green
- diff_del = "\033[38;2;205;0;0m" # red
- quote_1 = "\033[38;2;95;175;255m" # blue
- quote_2 = "\033[38;2;255;135;0m" # orange
- quote_3 = "\033[38;2;175;135;255m" # purple
- quote_4 = "\033[38;2;255;95;215m" # pink
- quote_x = "\033[38;2;128;128;128m" # gray
- bold = "\033[1m"
- reset = "\033[0m"
- } else if (theme == "terminal") {
- # terminal respects the users configured terminal color theme
- url = "\033[4;34m" # underline blue
- header = "\033[35m" # magenta
- signature = "\033[35m" # magenta
- diff_meta = "\033[2m" # faint
- diff_chunk = "\033[36m" # cyan
- diff_add = "\033[32m" # green
- diff_del = "\033[31m" # red
- quote_1 = "\033[37m" # grey
- quote_2 = "\033[34m" # blue
- quote_3 = "\033[2;37m" # faint grey
- quote_4 = "\033[2;34m" # faint blue
- quote_x = "\033[2;37m" # faint grey
- bold = "\033[1m"
- reset = "\033[0m"
- } else {
- print "error: unknown theme " theme > "/dev/stderr"
- exit 1
- }
- # state
- in_diff = 0
- in_signature = 0
- in_headers = 0
- in_body = 0
- # patterns
- header_pattern = "^[A-Z][[:alnum:]-]+:"
- url_pattern = "[[:lower:]]+://[[:graph:]]+|(mailto:)?[[:alnum:]_\\+\\.~/-]*[[:alnum:]_]@[[:lower:]][[:alnum:]\\.-]*[[:lower:]]"
- meta_pattern = "^(diff --git|(new|deleted) file|similarity index|(rename|copy) (to|from)|index|---|\\+\\+\\+) "
-}
-function color_quote(line) {
- level = 0
- quotes = ""
- while (line ~ /^>/) {
- level += 1
- quotes = quotes ">"
- line = substr(line, 2)
- while (line ~ /^ /) {
- quotes = quotes " "
- line = substr(line, 2)
- }
- }
- if (level == 1) {
- color = quote_1
- } else if (level == 2) {
- color = quote_2
- } else if (level == 3) {
- color = quote_3
- } else if (level == 4) {
- color = quote_4
- } else {
- color = quote_x
- }
- if (match(line, meta_pattern)) {
- return color quotes bold line reset
- } else if (line ~ /^\+/) {
- return color quotes diff_add line reset
- } else if (line ~ /^-/) {
- return color quotes diff_del line reset
- }
- gsub(url_pattern, url "&" color, line)
- return color quotes line reset
-}
-{
- # Strip carriage returns from line
- sub(/\r$/, "")
-
- if (in_diff) {
- if ($0 ~ /^-- ?$/) {
- in_diff = 0
- in_signature = 1
- $0 = signature $0 reset
- } else if ($0 ~ /^@@ /) {
- gsub(/^@@[^@]+@@/, diff_chunk "&" reset)
- } else if (match($0, meta_pattern)) {
- $0 = diff_meta $0 reset
- } else if ($0 ~ /^\+/) {
- $0 = diff_add $0 reset
- } else if ($0 ~ /^-/) {
- $0 = diff_del $0 reset
- } else if ($0 !~ /^ / && $0 !~ /^$/) {
- in_diff = 0
- in_body = 1
- if ($0 ~ /^>/) {
- $0 = color_quote($0)
- } else {
- gsub(url_pattern, url "&" reset)
- }
- }
- } else if (in_signature) {
- gsub(url_pattern, url "&" signature)
- $0 = signature $0 reset
- } else if (in_headers) {
- if ($0 ~ /^$/) {
- in_headers = 0
- in_body = 1
- } else {
- sub(header_pattern, header "&" reset)
- gsub(url_pattern, url "&" reset)
- }
- } else if (in_body) {
- if ($0 ~ /^>/) {
- $0 = color_quote($0)
- } else if ($0 ~ /^diff --git /) {
- in_body = 0
- in_diff = 1
- $0 = diff_meta $0 reset
- } else if ($0 ~ /^-- ?$/) {
- in_body = 0
- in_signature = 1
- $0 = signature $0 reset
- } else {
- gsub(url_pattern, url "&" reset)
- }
- } else if ($0 ~ /^diff --git /) {
- in_diff = 1
- $0 = diff_meta $0 reset
- } else if ($0 ~ /^-- ?$/) {
- in_signature = 1
- $0 = signature $0 reset
- } else if (match($0, header_pattern)) {
- in_headers = 1
- sub(header_pattern, header "&" reset)
- gsub(url_pattern, url "&" reset)
- } else {
- in_body = 1
- if ($0 ~ /^>/) {
- $0 = color_quote($0)
- } else {
- gsub(url_pattern, url "&" reset)
- }
- }
-
- print
-}
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;
+}
diff --git a/filters/vectors/colorize-patch.expected b/filters/vectors/colorize-patch.expected
new file mode 100644
index 00000000..1a2144a0
--- /dev/null
+++ b/filters/vectors/colorize-patch.expected
@@ -0,0 +1,49 @@
+From: Robin Jarry <robin@jarry.cc>
+Date: Mon, 26 Dec 2022 17:02:14 +0100
+Subject: [PATCH aerc] doc: fix numbered lists
+
+According to scdoc(5), numbered lists start with a period.
+
+Fixes: af63bd0188d1 ("doc: homogenize scdoc markup")
+Signed-off-by: Robin Jarry <robin@jarry.cc>
+---
+ doc/aerc-stylesets.7.scd | 18 +++++++++---------
+ 1 file changed, 9 insertions(+), 9 deletions(-)
+
+diff --git a/doc/aerc-stylesets.7.scd b/doc/aerc-stylesets.7.scd
+index d82ba7cf8163..34bbf4af0fc5 100644
+--- a/doc/aerc-stylesets.7.scd
++++ b/doc/aerc-stylesets.7.scd
+@@ -180,20 +180,20 @@ that style applies, unless overridden by a higher layer.
+
+ The order that *msglist_\** styles are applied in is, from first to last:
+
+-1. *msglist_default*
+-2. *msglist_unread*
+-3. *msglist_read*
+-4. *msglist_flagged*
+-5. *msglist_deleted*
+-6. *msglist_marked*
++. *msglist_default*
++. *msglist_unread*
++. *msglist_read*
++. *msglist_flagged*
++. *msglist_deleted*
++. *msglist_marked*
+
+ So, the marked style will override all other msglist styles.
+
+ The order for *dirlist_\** styles is:
+
+-1. *dirlist_default*
+-2. *dirlist_unread*
+-3. *dirlist_recent*
++. *dirlist_default*
++. *dirlist_unread*
++. *dirlist_recent*
+
+ ## COLORS
+
+-- 
+2.39.0
+
diff --git a/filters/vectors/colorize-patch.in b/filters/vectors/colorize-patch.in
new file mode 100644
index 00000000..48e12d8e
--- /dev/null
+++ b/filters/vectors/colorize-patch.in
@@ -0,0 +1,49 @@
+From: Robin Jarry <robin@jarry.cc>
+Date: Mon, 26 Dec 2022 17:02:14 +0100
+Subject: [PATCH aerc] doc: fix numbered lists
+
+According to scdoc(5), numbered lists start with a period.
+
+Fixes: af63bd0188d1 ("doc: homogenize scdoc markup")
+Signed-off-by: Robin Jarry <robin@jarry.cc>
+---
+ doc/aerc-stylesets.7.scd | 18 +++++++++---------
+ 1 file changed, 9 insertions(+), 9 deletions(-)
+
+diff --git a/doc/aerc-stylesets.7.scd b/doc/aerc-stylesets.7.scd
+index d82ba7cf8163..34bbf4af0fc5 100644
+--- a/doc/aerc-stylesets.7.scd
++++ b/doc/aerc-stylesets.7.scd
+@@ -180,20 +180,20 @@ that style applies, unless overridden by a higher layer.
+
+ The order that *msglist_\** styles are applied in is, from first to last:
+
+-1. *msglist_default*
+-2. *msglist_unread*
+-3. *msglist_read*
+-4. *msglist_flagged*
+-5. *msglist_deleted*
+-6. *msglist_marked*
++. *msglist_default*
++. *msglist_unread*
++. *msglist_read*
++. *msglist_flagged*
++. *msglist_deleted*
++. *msglist_marked*
+
+ So, the marked style will override all other msglist styles.
+
+ The order for *dirlist_\** styles is:
+
+-1. *dirlist_default*
+-2. *dirlist_unread*
+-3. *dirlist_recent*
++. *dirlist_default*
++. *dirlist_unread*
++. *dirlist_recent*
+
+ ## COLORS
+
+--
+2.39.0
+
diff --git a/filters/vectors/colorize-quotes.expected b/filters/vectors/colorize-quotes.expected
new file mode 100644
index 00000000..d350e4f1
--- /dev/null
+++ b/filters/vectors/colorize-quotes.expected
@@ -0,0 +1,65 @@
+Foo Bar, xxxxx:
+> Lorem ipsum dolor sit amet, insolens adolescens ne usu? In pri denique
+> argumentum, te autem decore convenire mea! Duo nisl esse an, aliquid
+> conceptam sea cu. Ignota copiosae gubergren ad est, ut illum doming vocibus
+> sed. Et vis nulla expetendis mediocritatem, errem option gloriatur at nam?
+> Brute vidisse corpora ut his, sonet omnesque adipiscing ea quo, cum ea errem
+> aliquip reformidans?
+
+Magna delicatissimi ei vel? Quem petentium scribentur eum ne? Et inani debet
+cetero mea, sint conceptam efficiendi mel te. Qui ut senserit interesset, per
+nibh petentium at! Sit docendi laboramus ei, animal insolens ad mea.
+
+>> Nostrud alienum nec in, illum errem audiam no per! Saepe alterum vis ea! Ei
+>> quis minim ius, ut eos mandamus salutandi. Lorem facilisis in nam, ridens
+>> principes sadipscing et eum, pri graecis singulis ut. Mea dolor primis
+>> impetus in, his epicurei tacimates id, vis labitur suscipit ad.
+> Erat alienum interpretaris has et, te vim aliquam molestie. Nam vivendum
+> facilisis qualisque at, ex his mucius qualisque! Fabulas lucilius adversarium
+> eu his. Cu soluta inermis accusata usu, his nulla dolore ne, vis id semper
+> detracto sententia <https://foobar.com> && "https://foobaz.org/".
+>
+> Error libris deleniti ea mei, vis at elit probo munere, his sint unum
+> albucius ex.
+
+Graece definiebas scripserit ne est? Nec nonumes explicari contentiones ne,
+vocent iuvaret placerat no vix. Nec et partem salutandi deseruisse, his no
+possim malorum pericula. Te quando reprehendunt nam, at consul sadipscing vel?
+Velit possim aliquando ei per, ne simul quodsi antiopam sea, ullum choro
+facilisi et pri!
+
+> Dico soleat partem ea pro, ad vix impetus splendide. Primis melius principes
+> pri ad, tacimates pertinacia ei pro? Appareat atomorum oportere at nam, eu
+> per quod minim reprimique, ornatus graecis ad vel. Malis vulputate ea qui,
+> eum tacimates recteque et, usu ea dolore vidisse. Brute mediocrem molestiae
+> sed te. No stet prompta pri, rebum populo nominati eos te.
+>
+> diff --git a/foo b/foo
+> index 4b0fe8dded3a..518b67134639 100644
+> --- a/foo
+> +++ b/foo
+> @@ -131,6 +131,83 @@ func pouet() int {
+> err := doThis()
+> 
+> - err2 := doThat()
+> + err2 := notDoThat()
+> 
+> if err != nil || err2 != nil {
+
+Id vix referrentur philosophia, veri labores an nec. Noster denique no duo, sit
+ei diam inermis vocibus! Mutat principes ex pro, at pericula assueverit vel.
+Has putent verterem constituto ex, tale electram duo at! Ei nulla lucilius
+intellegat nam, pro quod epicuri dissentiet ut, omnis voluptatibus definitiones
+vim at.
+
+https://git-man-page-generator.lokaltog.net/#Y2xhcCQkY29tbWFuZA==
+
+Eam mundi libris debitis ad, eam regione numquam at. Eum omnes bonorum eu,
+oporteat assueverit disputationi nam ne, nonumes iracundia mea ad! Duo libris
+recusabo id, ceteros salutatus inciderint vim ea. Et graeco reformidans vel? Ei
+has labore quidam?
+
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> sympa, non?
+
+-- 
+Batman
diff --git a/filters/vectors/colorize-quotes.in b/filters/vectors/colorize-quotes.in
new file mode 100644
index 00000000..31b04911
--- /dev/null
+++ b/filters/vectors/colorize-quotes.in
@@ -0,0 +1,65 @@
+Foo Bar, xxxxx:
+> Lorem ipsum dolor sit amet, insolens adolescens ne usu? In pri denique
+> argumentum, te autem decore convenire mea! Duo nisl esse an, aliquid
+> conceptam sea cu. Ignota copiosae gubergren ad est, ut illum doming vocibus
+> sed. Et vis nulla expetendis mediocritatem, errem option gloriatur at nam?
+> Brute vidisse corpora ut his, sonet omnesque adipiscing ea quo, cum ea errem
+> aliquip reformidans?
+
+Magna delicatissimi ei vel? Quem petentium scribentur eum ne? Et inani debet
+cetero mea, sint conceptam efficiendi mel te. Qui ut senserit interesset, per
+nibh petentium at! Sit docendi laboramus ei, animal insolens ad mea.
+
+>> Nostrud alienum nec in, illum errem audiam no per! Saepe alterum vis ea! Ei
+>> quis minim ius, ut eos mandamus salutandi. Lorem facilisis in nam, ridens
+>> principes sadipscing et eum, pri graecis singulis ut. Mea dolor primis
+>> impetus in, his epicurei tacimates id, vis labitur suscipit ad.
+> Erat alienum interpretaris has et, te vim aliquam molestie. Nam vivendum
+> facilisis qualisque at, ex his mucius qualisque! Fabulas lucilius adversarium
+> eu his. Cu soluta inermis accusata usu, his nulla dolore ne, vis id semper
+> detracto sententia <https://foobar.com> && "https://foobaz.org/".
+>
+> Error libris deleniti ea mei, vis at elit probo munere, his sint unum
+> albucius ex.
+
+Graece definiebas scripserit ne est? Nec nonumes explicari contentiones ne,
+vocent iuvaret placerat no vix. Nec et partem salutandi deseruisse, his no
+possim malorum pericula. Te quando reprehendunt nam, at consul sadipscing vel?
+Velit possim aliquando ei per, ne simul quodsi antiopam sea, ullum choro
+facilisi et pri!
+
+> Dico soleat partem ea pro, ad vix impetus splendide. Primis melius principes
+> pri ad, tacimates pertinacia ei pro? Appareat atomorum oportere at nam, eu
+> per quod minim reprimique, ornatus graecis ad vel. Malis vulputate ea qui,
+> eum tacimates recteque et, usu ea dolore vidisse. Brute mediocrem molestiae
+> sed te. No stet prompta pri, rebum populo nominati eos te.
+>
+> diff --git a/foo b/foo
+> index 4b0fe8dded3a..518b67134639 100644
+> --- a/foo
+> +++ b/foo
+> @@ -131,6 +131,83 @@ func pouet() int {
+> err := doThis()
+>
+> - err2 := doThat()
+> + err2 := notDoThat()
+>
+> if err != nil || err2 != nil {
+
+Id vix referrentur philosophia, veri labores an nec. Noster denique no duo, sit
+ei diam inermis vocibus! Mutat principes ex pro, at pericula assueverit vel.
+Has putent verterem constituto ex, tale electram duo at! Ei nulla lucilius
+intellegat nam, pro quod epicuri dissentiet ut, omnis voluptatibus definitiones
+vim at.
+
+https://git-man-page-generator.lokaltog.net/#Y2xhcCQkY29tbWFuZA==
+
+Eam mundi libris debitis ad, eam regione numquam at. Eum omnes bonorum eu,
+oporteat assueverit disputationi nam ne, nonumes iracundia mea ad! Duo libris
+recusabo id, ceteros salutatus inciderint vim ea. Et graeco reformidans vel? Ei
+has labore quidam?
+
+>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> sympa, non?
+
+--
+Batman