aboutsummaryrefslogblamecommitdiffstats
path: root/filters/colorize.c
blob: 307fe1905a50887e997e01b94e9c213e882f6cf4 (plain) (tree)



























































































































































                                                                         











                                                                           
































































































































































































































































                                                                                                      

                                                                           
















                                                                             

                                                               










































































































































































































































                                                                            
/* 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)
				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;
}