ledit

Text editor (WIP)
git clone git://lumidify.org/ledit.git (fast, but not encrypted)
git clone https://lumidify.org/ledit.git (encrypted, but very slow)
git clone git://4kcetb7mo7hj6grozzybxtotsub5bempzo4lirzc3437amof2c2impyd.onion/ledit.git (over tor)
Log | Files | Refs | README | LICENSE

configparser.c (63996B)


      1 #ifdef LEDIT_DEBUG
      2 #include <time.h>
      3 #include "macros.h"
      4 #endif
      5 #include <stdio.h>
      6 #include <ctype.h>
      7 #include <errno.h>
      8 #include <string.h>
      9 #include <stdint.h>
     10 #include <stdlib.h>
     11 #include <limits.h>
     12 
     13 #include "util.h"
     14 #include "memory.h"
     15 #include "assert.h"
     16 #include "configparser.h"
     17 #include "theme_config.h"
     18 #include "keys_config.h"
     19 
     20 /* FIXME: Replace this entire parser with something sensible.
     21    The current handwritten parser is mainly for the lulz. */
     22 
     23 /* FIXME: standardize error messages */
     24 /* FIXME: it isn't entirely correct to give size_t as length for
     25    string in print_fmt (supposed to be int) */
     26 
     27 struct config {
     28 	ledit_theme *theme;
     29 	basic_key_array *basic_keys;
     30 	command_key_array *command_keys;
     31 	command_array *cmds;
     32 	char **langs;
     33 	size_t num_langs;
     34 	size_t alloc_langs;
     35 } config = {NULL, NULL, NULL, NULL, NULL, 0, 0};
     36 
     37 enum toktype {
     38 	STRING,
     39 	LBRACE,
     40 	RBRACE,
     41 	EQUALS,
     42 	NEWLINE,
     43 	ERROR,
     44 	END
     45 };
     46 
     47 static const char *
     48 toktype_str(enum toktype type) {
     49 	switch (type) {
     50 	case STRING:
     51 		return "string";
     52 		break;
     53 	case LBRACE:
     54 		return "left brace";
     55 		break;
     56 	case RBRACE:
     57 		return "right brace";
     58 		break;
     59 	case EQUALS:
     60 		return "equals";
     61 		break;
     62 	case NEWLINE:
     63 		return "newline";
     64 		break;
     65 	case ERROR:
     66 		return "error";
     67 		break;
     68 	case END:
     69 		return "end of file";
     70 		break;
     71 	default:
     72 		return "unknown";
     73 	}
     74 }
     75 
     76 struct token {
     77 	char *text;
     78 	size_t len;
     79 	enum toktype type;
     80 	size_t line;        /* line in original input */
     81 	size_t line_offset; /* offset from start of line */
     82 };
     83 
     84 struct lexstate {
     85 	char *text;
     86 	size_t len;        /* length of text */
     87 	size_t cur;        /* current byte position */
     88 	size_t cur_line;   /* current line */
     89 	size_t line_start; /* byte offset of start of current line */
     90 };
     91 
     92 static struct token
     93 next_token(struct lexstate *s) {
     94 	char c;
     95 	struct token tok;
     96 	while (1) {
     97 		if (s->cur >= s->len)
     98 			return (struct token){NULL, 0, END, s->cur_line, s->cur - s->line_start + 1};
     99 		while (isspace(c = s->text[s->cur])) {
    100 			s->cur++;
    101 			if (c == '\n') {
    102 				struct token tok = (struct token){s->text + s->cur - 1, 1, NEWLINE, s->cur_line, s->cur - s->line_start};
    103 				s->cur_line++;
    104 				s->line_start = s->cur;
    105 				return tok;
    106 			}
    107 			if (s->cur >= s->len)
    108 				return (struct token){NULL, 0, END, s->cur_line, s->cur - s->line_start + 1};
    109 		}
    110 
    111 		switch (s->text[s->cur]) {
    112 		case '#':
    113 			s->cur++;
    114 			while (s->cur < s->len && s->text[s->cur] != '\n')
    115 				s->cur++;
    116 			continue;
    117 		case '{':
    118 			tok = (struct token){s->text + s->cur, 1, LBRACE, s->cur_line, s->cur - s->line_start + 1};
    119 			s->cur++;
    120 			break;
    121 		case '}':
    122 			tok = (struct token){s->text + s->cur, 1, RBRACE, s->cur_line, s->cur - s->line_start + 1};
    123 			s->cur++;
    124 			break;
    125 		case '=':
    126 			tok = (struct token){s->text + s->cur, 1, EQUALS, s->cur_line, s->cur - s->line_start + 1};
    127 			s->cur++;
    128 			break;
    129 		case '"':
    130 			/* FIXME: error if next char is not whitespace or end */
    131 			s->cur++;
    132 			tok = (struct token){s->text + s->cur, 0, STRING, s->cur_line, s->cur - s->line_start + 1};
    133 			size_t shift = 0, bs = 0;
    134 			int finished = 0;
    135 			while (s->cur < s->len) {
    136 				char c = s->text[s->cur];
    137 				if (c == '\n') {
    138 					break;
    139 				} else if (c == '\\') {
    140 					shift += bs;
    141 					tok.len += bs;
    142 					bs = (bs + 1) % 2;
    143 				} else if (c == '"') {
    144 					if (bs) {
    145 						shift++;
    146 						tok.len++;
    147 						bs = 0;
    148 					} else {
    149 						s->cur++;
    150 						finished = 1;
    151 						break;
    152 					}
    153 				} else {
    154 					tok.len++;
    155 				}
    156 				s->text[s->cur - shift] = s->text[s->cur];
    157 				s->cur++;
    158 			}
    159 			if (!finished) {
    160 				tok.text = "Unfinished string";
    161 				tok.len = strlen("Unfinished string");
    162 				tok.type = ERROR;
    163 			}
    164 			break;
    165 		default:
    166 			tok = (struct token){s->text + s->cur, 1, STRING, s->cur_line, s->cur - s->line_start + 1};
    167 			s->cur++;
    168 			while (s->cur < s->len) {
    169 				char c = s->text[s->cur];
    170 				if (isspace(c) || c == '{' || c == '}' || c == '=') {
    171 					break;
    172 				} else if (c == '"') {
    173 					tok.text = "Unexpected start of string";
    174 					tok.len = strlen("Unexpected start of string");
    175 					tok.type = ERROR;
    176 					tok.line_offset = s->cur - s->line_start + 1;
    177 				}
    178 				tok.len++;
    179 				s->cur++;
    180 			}
    181 		}
    182 		return tok;
    183 	}
    184 }
    185 
    186 typedef struct ast_obj ast_obj;
    187 
    188 typedef struct {
    189 	ast_obj *objs;
    190 	size_t len, cap;
    191 } ast_list;
    192 
    193 typedef struct {
    194 	struct token tok;
    195 } ast_string;
    196 
    197 typedef struct {
    198 	struct token tok;
    199 	ast_obj *value;
    200 } ast_assignment;
    201 
    202 typedef struct {
    203 	struct token func_tok;
    204 	struct token *args;
    205 	size_t len, cap;
    206 } ast_statement;
    207 
    208 enum objtype {
    209 	OBJ_LIST,
    210 	OBJ_STRING,
    211 	OBJ_ASSIGNMENT,
    212 	OBJ_STATEMENT
    213 };
    214 
    215 struct ast_obj {
    216 	struct token tok;
    217 	union {
    218 		ast_list list;
    219 		ast_string str;
    220 		ast_assignment assignment;
    221 		ast_statement statement;
    222 	} obj;
    223 	enum objtype type;
    224 };
    225 
    226 /* Note: These functions only free everything inside the object
    227    so they can be used with stack variables (or array elements)! */
    228 
    229 static void destroy_obj(ast_obj *obj);
    230 
    231 static void
    232 destroy_list(ast_list *list) {
    233 	if (!list)
    234 		return;
    235 	for (size_t i = 0; i < list->len; i++) {
    236 		destroy_obj(&list->objs[i]);
    237 	}
    238 	free(list->objs);
    239 	list->objs = NULL;
    240 	list->len = list->cap = 0;
    241 }
    242 
    243 static void
    244 destroy_obj(ast_obj *obj) {
    245 	if (!obj)
    246 		return;
    247 	switch (obj->type) {
    248 		case OBJ_LIST:
    249 			destroy_list(&obj->obj.list);
    250 			break;
    251 		case OBJ_ASSIGNMENT:
    252 			destroy_obj(obj->obj.assignment.value);
    253 			free(obj->obj.assignment.value);
    254 			obj->obj.assignment.value = NULL;
    255 			break;
    256 		case OBJ_STATEMENT:
    257 			free(obj->obj.statement.args);
    258 			obj->obj.statement.args = NULL;
    259 			obj->obj.statement.len = obj->obj.statement.cap = 0;
    260 			break;
    261 		default:
    262 			break;
    263 	}
    264 }
    265 
    266 /* FIXME: overflow */
    267 static void
    268 list_append(ast_list *list, ast_obj o) {
    269 	list->cap = ideal_array_size(list->cap, add_sz(list->len, 1));
    270 	list->objs = ledit_reallocarray(list->objs, list->cap, sizeof(ast_obj));
    271 	list->objs[list->len++] = o;
    272 }
    273 
    274 static void
    275 statement_append(ast_statement *statement, struct token tok) {
    276 	statement->cap = ideal_array_size(statement->cap, add_sz(statement->len, 1));
    277 	statement->args = ledit_reallocarray(statement->args, statement->cap, sizeof(struct token));
    278 	statement->args[statement->len++] = tok;
    279 }
    280 
    281 /* FIXME: make this a bit nicer */
    282 /* Note: A lot of the ugliness is because of the
    283    (failed) attempt to somewhat optimize everything */
    284 
    285 static int
    286 parse_list(struct lexstate *s, ast_list *ret, int implicit_end, char *filename, char **errstr) {
    287 	*ret = (ast_list){NULL, 0, 0};
    288 	struct token tok = next_token(s);
    289 	struct token tok2;
    290 	while (1) {
    291 		switch (tok.type) {
    292 		case STRING:
    293 			tok2 = next_token(s);
    294 			if (tok2.type == STRING) {
    295 				ast_statement statement = {tok, NULL, 0, 0};
    296 				/* FIXME: maybe allow lists in statements? */
    297 				while (tok2.type == STRING) {
    298 					statement_append(&statement, tok2);
    299 					tok2 = next_token(s);
    300 				}
    301 				list_append(ret, (ast_obj){.tok = tok, .obj = {.statement = statement}, .type = OBJ_STATEMENT});
    302 				tok = tok2;
    303 			} else if (tok2.type == EQUALS) {
    304 				ast_assignment assignment = {tok, NULL};
    305 				assignment.value = ledit_malloc(sizeof(ast_obj));
    306 				tok2 = next_token(s);
    307 				assignment.value->tok = tok2;
    308 				struct token orig_tok = tok;
    309 				if (tok2.type == STRING) {
    310 					assignment.value->obj.str = (ast_string){tok2};
    311 					assignment.value->type = OBJ_STRING;
    312 					tok = next_token(s);
    313 					if (tok.type == STRING) {
    314 						*errstr = print_fmt(
    315 						    "%s: Invalid assignment at line %zu, offset %zu",
    316 						    filename, tok.line, tok.line_offset
    317 						);
    318 						free(assignment.value);
    319 						goto error;
    320 					}
    321 				} else if (tok2.type == LBRACE) {
    322 					assignment.value->type = OBJ_LIST;
    323 					/* just in case */
    324 					assignment.value->obj.list = (ast_list){NULL, 0, 0};
    325 					if (parse_list(s, &assignment.value->obj.list, 0, filename, errstr)) {
    326 						free(assignment.value);
    327 						goto error;
    328 					}
    329 					tok = next_token(s);
    330 					if (tok.type == STRING) {
    331 						*errstr = print_fmt(
    332 						    "%s: Invalid assignment at line %zu, offset %zu",
    333 						    filename, tok.line, tok.line_offset
    334 						);
    335 						destroy_list(&assignment.value->obj.list);
    336 						free(assignment.value);
    337 						goto error;
    338 					}
    339 				} else {
    340 					*errstr = print_fmt(
    341 					    "%s: Invalid assignment at line %zu, offset %zu",
    342 					    filename, tok2.line, tok2.line_offset
    343 					);
    344 					free(assignment.value);
    345 					goto error;
    346 				}
    347 				list_append(ret, (ast_obj){.tok = orig_tok, .obj = {.assignment = assignment}, .type = OBJ_ASSIGNMENT});
    348 			} else {
    349 				*errstr = print_fmt(
    350 				    "%s: Invalid token '%s' at line %zu, offset %zu",
    351 				    filename, toktype_str(tok2.type), tok2.line, tok2.line_offset
    352 				);
    353 				goto error;
    354 			}
    355 			break;
    356 		case NEWLINE:
    357 			tok = next_token(s);
    358 			break;
    359 		case RBRACE:
    360 			if (implicit_end) {
    361 				*errstr = print_fmt(
    362 				    "%s: Unexpected right brace at line %zu, offset %zu",
    363 				    filename, tok.line, tok.line_offset
    364 				);
    365 				goto error;
    366 			} else {
    367 				return 0;
    368 			}
    369 		case END:
    370 			if (!implicit_end) {
    371 				*errstr = print_fmt(
    372 				    "%s: Unexpected end of file at line %zu, offset %zu",
    373 				    filename, tok.line, tok.line_offset
    374 				);
    375 				goto error;
    376 			} else {
    377 				return 0;
    378 			}
    379 		case LBRACE:
    380 		case EQUALS:
    381 		case ERROR:
    382 		default:
    383 			*errstr = print_fmt(
    384 			    "%s: Unexpected token '%s' at line %zu, offset %zu",
    385 			    filename, toktype_str(tok.type), tok.line, tok.line_offset
    386 			);
    387 			goto error;
    388 		}
    389 	}
    390 	return 0;
    391 error:
    392 	destroy_list(ret);
    393 	return 1;
    394 }
    395 
    396 static char *
    397 load_file(char *filename, size_t *len_ret, char **errstr) {
    398 	long len;
    399 	char *file_contents;
    400 	FILE *file;
    401 
    402 	/* FIXME: https://wiki.sei.cmu.edu/confluence/display/c/FIO19-C.+Do+not+use+fseek()+and+ftell()+to+compute+the+size+of+a+regular+file */
    403 	file = fopen(filename, "r");
    404 	if (!file) goto error;
    405 	if (fseek(file, 0, SEEK_END)) goto errorclose;
    406 	len = ftell(file);
    407 	if (len < 0) goto errorclose;
    408 	if (fseek(file, 0, SEEK_SET)) goto errorclose;
    409 	file_contents = ledit_malloc(add_sz((size_t)len, 1));
    410 	clearerr(file);
    411 	fread(file_contents, 1, (size_t)len, file);
    412 	if (ferror(file)) goto errorclose;
    413 	file_contents[len] = '\0';
    414 	if (fclose(file)) goto error;
    415 	*len_ret = (size_t)len;
    416 	return file_contents;
    417 error:
    418 	if (errstr)
    419 		*errstr = strerror(errno);
    420 	return NULL;
    421 errorclose:
    422 	if (errstr)
    423 		*errstr = strerror(errno);
    424 	fclose(file);
    425 	return NULL;
    426 }
    427 
    428 /* FIXME: max recursion depth in parser */
    429 
    430 static int
    431 parse_theme_color(
    432     ledit_common *common,
    433     void *obj, const char *val, size_t val_len, char *key,
    434     char *filename, size_t line, size_t line_offset, char **errstr) {
    435 	XftColor *dst = (XftColor *)obj;
    436 	char col[8]; /* 7 for '#' and 6 hex values + 1 for '\0' */
    437 	if (val_len == 7 && val[0] == '#') {
    438 		strncpy(col, val, val_len);
    439 		col[val_len] = '\0';
    440 	} else if (val_len == 6) {
    441 		col[0] = '#';
    442 		strncpy(col + 1, val, val_len);
    443 		col[val_len + 1] = '\0';
    444 	} else {
    445 		goto error;
    446 	}
    447 	/* FIXME: XftColorAllocValue */
    448 	if (!XftColorAllocName(common->dpy, common->vis, common->cm, col, dst))
    449 		goto error;
    450 	return 0;
    451 error:
    452 	*errstr = print_fmt(
    453 	    "%s: Unable to parse color specification "
    454 	    "'%.*s' for '%s' at line %zu, position %zu",
    455 	    filename, val_len, val, key, line, line_offset
    456 	);
    457 	return 1;
    458 }
    459 
    460 static void
    461 destroy_theme_color(ledit_common *common, void *obj) {
    462 	XftColor *color = (XftColor *)obj;
    463 	XftColorFree(common->dpy, common->vis, common->cm, color);
    464 }
    465 
    466 /* based partially on OpenBSD's strtonum */
    467 static int
    468 parse_theme_number(
    469     ledit_common *common,
    470     void *obj, const char *val, size_t val_len, char *key,
    471     char *filename, size_t line, size_t line_offset, char **errstr) {
    472 	(void)common;
    473 	int *num = (int *)obj;
    474 	/* the string needs to be nul-terminated
    475 	   if it contains more than 9 digits, it's illegal anyways */
    476 	if (val_len > 9)
    477 		goto error;
    478 	char str[10];
    479 	strncpy(str, val, val_len);
    480 	str[val_len] = '\0';
    481 	char *end;
    482 	long l = strtol(str, &end, 10);
    483         if (str == end || *end != '\0' ||
    484             l < 0 || l > INT_MAX || ((l == LONG_MIN ||
    485             l == LONG_MAX) && errno == ERANGE)) {
    486 		goto error;
    487         }
    488 	*num = (int)l;
    489 	return 0;
    490 error:
    491 	*errstr = print_fmt(
    492 	    "%s: Invalid number '%.*s' "
    493 	    "for '%s' at line %zu, position %zu",
    494 	    filename, val_len, val, key, line, line_offset
    495 	);
    496 	return 1;
    497 }
    498 
    499 static void
    500 destroy_theme_number(ledit_common *common, void *obj) {
    501 	(void)common;
    502 	(void)obj;
    503 }
    504 
    505 static int
    506 parse_theme_string(
    507     ledit_common *common,
    508     void *obj, const char *val, size_t val_len, char *key,
    509     char *filename, size_t line, size_t line_offset, char **errstr) {
    510 	(void)common; (void)key;
    511 	(void)filename; (void)line; (void)line_offset; (void)errstr;
    512 
    513 	char **obj_str = (char **)obj;
    514 	*obj_str = ledit_strndup(val, val_len);
    515 	return 0;
    516 }
    517 
    518 static void
    519 destroy_theme_string(ledit_common *common, void *obj) {
    520 	(void)common;
    521 	char **obj_str = (char **)obj;
    522 	free(*obj_str);
    523 }
    524 
    525 static int
    526 parse_theme_bool(
    527     ledit_common *common,
    528     void *obj, const char *val, size_t val_len, char *key,
    529     char *filename, size_t line, size_t line_offset, char **errstr) {
    530 	(void)common;
    531 	int *num = (int *)obj;
    532 	if (str_array_equal("true", val, val_len)) {
    533 		*num = 1;
    534 		return 0;
    535 	} else if (str_array_equal("false", val, val_len)) {
    536 		*num = 0;
    537 		return 0;
    538 	}
    539 	*errstr = print_fmt(
    540 	    "%s: Invalid boolean '%.*s' "
    541 	    "for '%s' at line %zu, position %zu",
    542 	    filename, val_len, val, key, line, line_offset
    543 	);
    544 	return 1;
    545 }
    546 
    547 static void
    548 destroy_theme_bool(ledit_common *common, void *obj) {
    549 	(void)common;
    550 	(void)obj;
    551 }
    552 
    553 /* FIXME: This interface is absolutely horrible - it's mainly this way to reuse the
    554    theme array for the destroy function */
    555 /* If theme is NULL, a new theme is loaded, else it is destroyed */
    556 static ledit_theme *
    557 load_destroy_theme(ledit_common *common, ast_list *theme_list, ledit_theme *theme, char *filename, char **errstr) {
    558 	*errstr = NULL;
    559 	int default_init = theme ? 1 : 0;
    560 	if (!theme)
    561 		theme = ledit_malloc(sizeof(ledit_theme));
    562 
    563 	struct {
    564 		char *key;
    565 		void *obj;
    566 		int (*parse_func)(
    567 		    ledit_common *common,
    568 		    void *obj, const char *val, size_t val_len, char *key,
    569 		    char *filename, size_t line, size_t line_offset, char **errstr
    570 		);
    571 		void (*destroy_func)(ledit_common *common, void *obj);
    572 		const char *default_value;
    573 		int initialized;
    574 	} settings[] = {
    575 		{"text-font", &theme->text_font, &parse_theme_string, &destroy_theme_string, TEXT_FONT, default_init},
    576 		{"text-size", &theme->text_size, &parse_theme_number, &destroy_theme_number, TEXT_SIZE, default_init},
    577 		{"scrollbar-width", &theme->scrollbar_width, &parse_theme_number, &destroy_theme_number, SCROLLBAR_WIDTH, default_init},
    578 		{"scrollbar-step", &theme->scrollbar_step, &parse_theme_number, &destroy_theme_number, SCROLLBAR_STEP, default_init},
    579 		{"extra-line-spacing", &theme->extra_line_spacing, &parse_theme_number, &destroy_theme_number, EXTRA_LINE_SPACING, default_init},
    580 		{"text-fg", &theme->text_fg, &parse_theme_color, &destroy_theme_color, TEXT_FG, default_init},
    581 		{"text-bg", &theme->text_bg, &parse_theme_color, &destroy_theme_color, TEXT_BG, default_init},
    582 		{"cursor-fg", &theme->cursor_fg, &parse_theme_color, &destroy_theme_color, CURSOR_FG, default_init},
    583 		{"cursor-bg", &theme->cursor_bg, &parse_theme_color, &destroy_theme_color, CURSOR_BG, default_init},
    584 		{"selection-fg", &theme->selection_fg, &parse_theme_color, &destroy_theme_color, SELECTION_FG, default_init},
    585 		{"selection-bg", &theme->selection_bg, &parse_theme_color, &destroy_theme_color, SELECTION_BG, default_init},
    586 		{"bar-fg", &theme->bar_fg, &parse_theme_color, &destroy_theme_color, BAR_FG, default_init},
    587 		{"bar-bg", &theme->bar_bg, &parse_theme_color, &destroy_theme_color, BAR_BG, default_init},
    588 		{"bar-cursor", &theme->bar_cursor, &parse_theme_color, &destroy_theme_color, BAR_CURSOR, default_init},
    589 		{"bar-fmt", &theme->bar_fmt, &parse_theme_string, &destroy_theme_string, BAR_FMT, default_init},
    590 		{"scrollbar-fg", &theme->scrollbar_fg, &parse_theme_color, &destroy_theme_color, SCROLLBAR_FG, default_init},
    591 		{"scrollbar-bg", &theme->scrollbar_bg, &parse_theme_color, &destroy_theme_color, SCROLLBAR_BG, default_init},
    592 		{"highlight-search", &theme->highlight_search, &parse_theme_bool, &destroy_theme_bool, HIGHLIGHT_SEARCH, default_init},
    593 	};
    594 
    595 	if (default_init)
    596 		goto cleanup;
    597 
    598 	if (theme_list) {
    599 		for (size_t i = 0; i < theme_list->len; i++) {
    600 			size_t line = theme_list->objs[i].tok.line;
    601 			size_t line_offset = theme_list->objs[i].tok.line_offset;
    602 			if (theme_list->objs[i].type != OBJ_ASSIGNMENT) {
    603 				*errstr = print_fmt(
    604 				    "%s: Invalid statement in theme configuration "
    605 				    "at line %zu, offset %zu", filename, line, line_offset
    606 				);
    607 				goto cleanup;
    608 			} else if (theme_list->objs[i].obj.assignment.value->type != OBJ_STRING) {
    609 				*errstr = print_fmt(
    610 				    "%s: Invalid assignment in theme configuration "
    611 				    "at line %zu, offset %zu", filename, line, line_offset
    612 				);
    613 				goto cleanup;
    614 			}
    615 
    616 			char *key = theme_list->objs[i].obj.assignment.tok.text;
    617 			size_t key_len = theme_list->objs[i].obj.assignment.tok.len;
    618 			char *val = theme_list->objs[i].obj.assignment.value->obj.str.tok.text;
    619 			size_t val_len = theme_list->objs[i].obj.assignment.value->obj.str.tok.len;
    620 
    621 			int found = 0;
    622 			/* FIXME: use binary search maybe */
    623 			for (size_t j = 0; j < LENGTH(settings); j++) {
    624 				if (str_array_equal(settings[j].key, key, key_len)) {
    625 					/* FIXME: maybe just make this a warning? */
    626 					if (settings[j].initialized) {
    627 						*errstr = print_fmt(
    628 						    "%s: Duplicate definition of "
    629 						    "'%.*s' at line %zu, position %zu",
    630 						    filename, key_len, key, line, line_offset
    631 						);
    632 						goto cleanup;
    633 					}
    634 					if (settings[j].parse_func(
    635 					    common, settings[j].obj, val, val_len,
    636 					    settings[j].key, filename, line, line_offset, errstr)) {
    637 						goto cleanup;
    638 					}
    639 					settings[j].initialized = 1;
    640 					found = 1;
    641 					break;
    642 				}
    643 			}
    644 			if (!found) {
    645 				*errstr = print_fmt(
    646 				    "%s: Invalid theme setting "
    647 				    "'%.*s' at line %zu, position %zu",
    648 				    filename, key_len, key, line, line_offset
    649 				);
    650 				goto cleanup;
    651 			}
    652 		}
    653 	}
    654 
    655 	for (size_t i = 0; i < LENGTH(settings); i++) {
    656 		if (!settings[i].initialized) {
    657 			/* FIXME: kind of inefficient to calculate strlen at runtime */
    658 			/* FIXME: line number doesn't make sense */
    659 			if (settings[i].parse_func(
    660 			    common, settings[i].obj, settings[i].default_value,
    661 			    strlen(settings[i].default_value), settings[i].key,
    662 			    "default config", 0, 0, errstr)) {
    663 				goto cleanup;
    664 			}
    665 		}
    666 	}
    667 
    668 	/* FIXME: make this check part of the generic handling above (also, < 0 is already checked anyways) */
    669 	/* FIXME: 100 is completely arbitrary */
    670 	if (theme->extra_line_spacing < 0 || theme->extra_line_spacing > 100) {
    671 		*errstr = print_fmt(
    672 		    "%s: Invalid value '%d' for theme setting 'extra-line-spacing' "
    673 		    "(allowed values are 0-100)",
    674 		    filename, theme->extra_line_spacing
    675 		);
    676 		goto cleanup;
    677 	}
    678 
    679 	return theme;
    680 cleanup:
    681 	for (size_t i = 0; i < LENGTH(settings); i++) {
    682 		if (settings[i].initialized) {
    683 			settings[i].destroy_func(common, settings[i].obj);
    684 		}
    685 	}
    686 	free(theme);
    687 	return NULL;
    688 }
    689 
    690 static ledit_theme *
    691 load_theme(ledit_common *common, ast_list *theme_list, char *filename, char **errstr) {
    692 	return load_destroy_theme(common, theme_list, NULL, filename, errstr);
    693 }
    694 
    695 static void
    696 destroy_theme(ledit_common *common, ledit_theme *theme) {
    697 	char *errstr = NULL;
    698 	if (!theme)
    699 		return;
    700 	(void)load_destroy_theme(common, NULL, theme, NULL, &errstr);
    701 	/* shouldn't happen... */
    702 	if (errstr)
    703 		free(errstr);
    704 }
    705 
    706 /* This only destroys the members inside 'cfg' since the config
    707  * struct itself is usually not on the heap. */
    708 static void
    709 config_destroy(ledit_common *common, struct config *cfg) {
    710 	if (cfg->theme)
    711 		destroy_theme(common, cfg->theme);
    712 	cfg->theme = NULL;
    713 	for (size_t i = 0; i < cfg->num_langs; i++) {
    714 		for (size_t j = 0; j < cfg->basic_keys[i].num_keys; j++) {
    715 			free(cfg->basic_keys[i].keys[j].text);
    716 		}
    717 		free(cfg->basic_keys[i].keys);
    718 		for (size_t j = 0; j < cfg->command_keys[i].num_keys; j++) {
    719 			free(cfg->command_keys[i].keys[j].text);
    720 		}
    721 		free(cfg->command_keys[i].keys);
    722 		for (size_t j = 0; j < cfg->cmds[i].num_cmds; j++) {
    723 			free(cfg->cmds[i].cmds[j].text);
    724 		}
    725 		free(cfg->cmds[i].cmds);
    726 		free(cfg->langs[i]);
    727 	}
    728 	free(cfg->basic_keys);
    729 	free(cfg->command_keys);
    730 	free(cfg->cmds);
    731 	free(cfg->langs);
    732 	cfg->basic_keys = NULL;
    733 	cfg->command_keys = NULL;
    734 	cfg->cmds = NULL;
    735 	cfg->langs = NULL;
    736 	cfg->num_langs = cfg->alloc_langs = 0;
    737 }
    738 
    739 void
    740 config_cleanup(ledit_common *common) {
    741 	config_destroy(common, &config);
    742 }
    743 
    744 /* FIXME: which additional ones are needed here? */
    745 static struct keysym_mapping {
    746 	char *name;
    747 	KeySym keysym;
    748 } keysym_map[] = {
    749 	{"backspace", XK_BackSpace},
    750 	{"begin", XK_Begin},
    751 	{"break", XK_Break},
    752 	{"cancel", XK_Cancel},
    753 	{"clear", XK_Clear},
    754 	{"delete", XK_Delete},
    755 	{"down", XK_Down},
    756 	{"end", XK_End},
    757 	{"escape", XK_Escape},
    758 	{"execute", XK_Execute},
    759 
    760 	{"f1", XK_F1},
    761 	{"f10", XK_F10},
    762 	{"f11", XK_F11},
    763 	{"f12", XK_F12},
    764 	{"f13", XK_F13},
    765 	{"f14", XK_F14},
    766 	{"f15", XK_F15},
    767 	{"f16", XK_F16},
    768 	{"f17", XK_F17},
    769 	{"f18", XK_F18},
    770 	{"f19", XK_F19},
    771 	{"f2", XK_F2},
    772 	{"f20", XK_F20},
    773 	{"f21", XK_F21},
    774 	{"f22", XK_F22},
    775 	{"f23", XK_F23},
    776 	{"f24", XK_F24},
    777 	{"f25", XK_F25},
    778 	{"f26", XK_F26},
    779 	{"f27", XK_F27},
    780 	{"f28", XK_F28},
    781 	{"f29", XK_F29},
    782 	{"f3", XK_F3},
    783 	{"f30", XK_F30},
    784 	{"f31", XK_F31},
    785 	{"f32", XK_F32},
    786 	{"f33", XK_F33},
    787 	{"f34", XK_F34},
    788 	{"f35", XK_F35},
    789 	{"f4", XK_F4},
    790 	{"f5", XK_F5},
    791 	{"f6", XK_F6},
    792 	{"f7", XK_F7},
    793 	{"f8", XK_F8},
    794 	{"f9", XK_F9},
    795 
    796 	{"find", XK_Find},
    797 	{"help", XK_Help},
    798 	{"home", XK_Home},
    799 	{"insert", XK_Insert},
    800 
    801 	{"kp-0", XK_KP_0},
    802 	{"kp-1", XK_KP_1},
    803 	{"kp-2", XK_KP_2},
    804 	{"kp-3", XK_KP_3},
    805 	{"kp-4", XK_KP_4},
    806 	{"kp-5", XK_KP_5},
    807 	{"kp-6", XK_KP_6},
    808 	{"kp-7", XK_KP_7},
    809 	{"kp-8", XK_KP_8},
    810 	{"kp-9", XK_KP_9},
    811 	{"kp-add", XK_KP_Add},
    812 	{"kp-begin", XK_KP_Begin},
    813 	{"kp-decimal", XK_KP_Decimal},
    814 	{"kp-delete", XK_KP_Delete},
    815 	{"kp-divide", XK_KP_Divide},
    816 	{"kp-down", XK_KP_Down},
    817 	{"kp-end", XK_KP_End},
    818 	{"kp-enter", XK_KP_Enter},
    819 	{"kp-equal", XK_KP_Equal},
    820 	{"kp-f1", XK_KP_F1},
    821 	{"kp-f2", XK_KP_F2},
    822 	{"kp-f3", XK_KP_F3},
    823 	{"kp-f4", XK_KP_F4},
    824 	{"kp-home", XK_KP_Home},
    825 	{"kp-insert", XK_KP_Insert},
    826 	{"kp-left", XK_KP_Left},
    827 	{"kp-multiply", XK_KP_Multiply},
    828 	{"kp-next", XK_KP_Next},
    829 	{"kp-page-down", XK_KP_Page_Down},
    830 	{"kp-page-up", XK_KP_Page_Up},
    831 	{"kp-prior", XK_KP_Prior},
    832 	{"kp-right", XK_KP_Right},
    833 	{"kp-separator", XK_KP_Separator},
    834 	{"kp-space", XK_KP_Space},
    835 	{"kp-subtract", XK_KP_Subtract},
    836 	{"kp-tab", XK_KP_Tab},
    837 	{"kp-up", XK_KP_Up},
    838 
    839 	{"l1", XK_L1},
    840 	{"l10", XK_L10},
    841 	{"l2", XK_L2},
    842 	{"l3", XK_L3},
    843 	{"l4", XK_L4},
    844 	{"l5", XK_L5},
    845 	{"l6", XK_L6},
    846 	{"l7", XK_L7},
    847 	{"l8", XK_L8},
    848 	{"l9", XK_L9},
    849 
    850 	{"left", XK_Left},
    851 	{"linefeed", XK_Linefeed},
    852 	{"menu", XK_Menu},
    853 	{"mode-switch", XK_Mode_switch},
    854 	{"next", XK_Next},
    855 	{"num-lock", XK_Num_Lock},
    856 	{"page-down", XK_Page_Down},
    857 	{"page-up", XK_Page_Up},
    858 	{"pause", XK_Pause},
    859 	{"print", XK_Print},
    860 	{"prior", XK_Prior},
    861 
    862 	{"r1", XK_R1},
    863 	{"r10", XK_R10},
    864 	{"r11", XK_R11},
    865 	{"r12", XK_R12},
    866 	{"r13", XK_R13},
    867 	{"r14", XK_R14},
    868 	{"r15", XK_R15},
    869 	{"r2", XK_R2},
    870 	{"r3", XK_R3},
    871 	{"r4", XK_R4},
    872 	{"r5", XK_R5},
    873 	{"r6", XK_R6},
    874 	{"r7", XK_R7},
    875 	{"r8", XK_R8},
    876 	{"r9", XK_R9},
    877 
    878 	{"redo", XK_Redo},
    879 	{"return", XK_Return},
    880 	{"right", XK_Right},
    881 	{"script-switch", XK_script_switch},
    882 	{"scroll-lock", XK_Scroll_Lock},
    883 	{"select", XK_Select},
    884 	{"space", XK_space},
    885 	{"sysreq", XK_Sys_Req},
    886 	{"tab", XK_Tab},
    887 	{"up", XK_Up},
    888 	{"undo", XK_Undo},
    889 };
    890 
    891 GEN_CB_MAP_HELPERS(keysym_map, struct keysym_mapping, name)
    892 
    893 static int
    894 parse_keysym(char *keysym_str, size_t len, KeySym *sym) {
    895 	struct keysym_mapping *km = keysym_map_get_entry(keysym_str, len);
    896 	if (!km)
    897 		return 1;
    898 	*sym = km->keysym;
    899 	return 0;
    900 }
    901 
    902 static int
    903 parse_modemask(char *modemask_str, size_t len, ledit_mode *mode_ret) {
    904 	size_t cur = 0;
    905 	*mode_ret = 0;
    906 	while (cur < len) {
    907 		if (str_array_equal("normal", modemask_str + cur, LEDIT_MIN(6, len - cur))) {
    908 			cur += 6;
    909 			*mode_ret |= NORMAL;
    910 		} else if (str_array_equal("visual", modemask_str + cur, LEDIT_MIN(6, len - cur))) {
    911 			cur += 6;
    912 			*mode_ret |= VISUAL;
    913 		} else if (str_array_equal("insert", modemask_str + cur, LEDIT_MIN(6, len - cur))) {
    914 			cur += 6;
    915 			*mode_ret |= INSERT;
    916 		} else {
    917 			return 1;
    918 		}
    919 		if (cur < len && modemask_str[cur] != '|')
    920 			return 1;
    921 		else
    922 			cur++;
    923 	}
    924 	return 0;
    925 }
    926 
    927 static int
    928 parse_modmask(char *modmask_str, size_t len, unsigned int *mask_ret) {
    929 	size_t cur = 0;
    930 	*mask_ret = 0;
    931 	while (cur < len) {
    932 		if (str_array_equal("shift", modmask_str + cur, LEDIT_MIN(5, len - cur))) {
    933 			cur += 5;
    934 			*mask_ret |= ShiftMask;
    935 		/*
    936 		} else if (str_array_equal("lock", modmask_str + cur, LEDIT_MIN(4, len - cur))) {
    937 			cur += 4;
    938 			*mask_ret |= LockMask;
    939 		*/
    940 		} else if (str_array_equal("control", modmask_str + cur, LEDIT_MIN(7, len - cur))) {
    941 			cur += 7;
    942 			*mask_ret |= ControlMask;
    943 		} else if (str_array_equal("mod1", modmask_str + cur, LEDIT_MIN(4, len - cur))) {
    944 			cur += 4;
    945 			*mask_ret |= Mod1Mask;
    946 		/*
    947 		} else if (str_array_equal("mod2", modmask_str + cur, LEDIT_MIN(4, len - cur))) {
    948 			cur += 4;
    949 			*mask_ret |= Mod2Mask;
    950 		*/
    951 		} else if (str_array_equal("mod3", modmask_str + cur, LEDIT_MIN(4, len - cur))) {
    952 			cur += 4;
    953 			*mask_ret |= Mod3Mask;
    954 		} else if (str_array_equal("mod4", modmask_str + cur, LEDIT_MIN(4, len - cur))) {
    955 			cur += 4;
    956 			*mask_ret |= Mod4Mask;
    957 		} else if (str_array_equal("mod5", modmask_str + cur, LEDIT_MIN(4, len - cur))) {
    958 			cur += 4;
    959 			*mask_ret |= Mod5Mask;
    960 		} else if (str_array_equal("any", modmask_str + cur, LEDIT_MIN(3, len - cur))) {
    961 			cur += 3;
    962 			*mask_ret = UINT_MAX;
    963 		} else {
    964 			return 1;
    965 		}
    966 		if (cur < len && modmask_str[cur] != '|')
    967 			return 1;
    968 		else
    969 			cur++;
    970 	}
    971 	return 0;
    972 }
    973 
    974 /* FIXME: it would probably be safer to not write the string lengths by hand... */
    975 static int
    976 parse_command_modemask(char *mode_str, size_t len, command_mode *mode_ret) {
    977 	size_t cur = 0;
    978 	*mode_ret = 0;
    979 	/* IMPORTANT: these need to be sorted appropriately so e.g. edit doesn't mess with edit-search */
    980 	while (cur < len) {
    981 		if (str_array_equal("substitute", mode_str + cur, LEDIT_MIN(10, len - cur))) {
    982 			cur += 10;
    983 			*mode_ret |= CMD_SUBSTITUTE;
    984 		} else if (str_array_equal("edit-search-backwards", mode_str + cur, LEDIT_MIN(21, len - cur))) {
    985 			cur += 21;
    986 			*mode_ret |= CMD_EDITSEARCHB;
    987 		} else if (str_array_equal("edit-search", mode_str + cur, LEDIT_MIN(11, len - cur))) {
    988 			cur += 11;
    989 			*mode_ret |= CMD_EDITSEARCH;
    990 		} else if (str_array_equal("edit", mode_str + cur, LEDIT_MIN(4, len - cur))) {
    991 			cur += 4;
    992 			*mode_ret |= CMD_EDIT;
    993 		} else {
    994 			return 1;
    995 		}
    996 		if (cur < len && mode_str[cur] != '|') {
    997 			return 1;
    998 		} else {
    999 			cur++;
   1000 		}
   1001 	}
   1002 	return 0;
   1003 }
   1004 
   1005 /* FIXME: generic dynamic array */
   1006 
   1007 static void
   1008 push_lang(struct config *cfg) {
   1009 	if (cfg->num_langs == cfg->alloc_langs) {
   1010 		cfg->alloc_langs = ideal_array_size(cfg->alloc_langs, add_sz(cfg->num_langs, 1));
   1011 		cfg->basic_keys = ledit_reallocarray(cfg->basic_keys, cfg->alloc_langs, sizeof(basic_key_array));
   1012 		cfg->command_keys = ledit_reallocarray(cfg->command_keys, cfg->alloc_langs, sizeof(command_key_array));
   1013 		cfg->cmds = ledit_reallocarray(cfg->cmds, cfg->alloc_langs, sizeof(command_array));
   1014 		cfg->langs = ledit_reallocarray(cfg->langs, cfg->alloc_langs, sizeof(char *));
   1015 	}
   1016 	basic_key_array *arr1 = &cfg->basic_keys[cfg->num_langs];
   1017 	arr1->keys = NULL;
   1018 	arr1->num_keys = arr1->alloc_keys = 0;
   1019 	command_key_array *arr2 = &cfg->command_keys[cfg->num_langs];
   1020 	arr2->keys = NULL;
   1021 	arr2->num_keys = arr2->alloc_keys = 0;
   1022 	command_array *arr3 = &cfg->cmds[cfg->num_langs];
   1023 	arr3->cmds = NULL;
   1024 	arr3->num_cmds = arr3->alloc_cmds = 0;
   1025 	cfg->langs[cfg->num_langs] = NULL;
   1026 	cfg->num_langs++;
   1027 }
   1028 
   1029 #define GEN_PARSE_STATEMENT(name, cb_type, mapping_type, mode_parse_func)                             \
   1030 static int                                                                                            \
   1031 name(ast_statement *st, mapping_type *m, char *filename, char **errstr) {                             \
   1032 	size_t line = st->func_tok.line;                                                              \
   1033 	size_t line_offset = st->func_tok.line_offset;                                                \
   1034 	m->cb = NULL;                                                                                 \
   1035 	m->text = NULL;                                                                               \
   1036 	m->mods = 0;                                                                                  \
   1037 	m->modes = 0;                                                                                 \
   1038 	m->keysym = 0;                                                                                \
   1039 	char *msg = NULL;                                                                             \
   1040 	if (!str_array_equal("bind", st->func_tok.text, st->func_tok.len) || st->len < 1) {           \
   1041 		msg = "Invalid statement";                                                            \
   1042 		goto error;                                                                           \
   1043 	}                                                                                             \
   1044 	m->cb = cb_type##_map_get_entry(st->args[0].text, st->args[0].len);                           \
   1045 	if (!m->cb) {                                                                                 \
   1046 		msg = "Invalid function specification";                                               \
   1047 		goto error;                                                                           \
   1048 	}                                                                                             \
   1049 	int text_init = 0, keysym_init = 0, modes_init = 0, mods_init = 0;                            \
   1050 	for (size_t i = 1; i < st->len; i++) {                                                        \
   1051 		line = st->args[i].line;                                                              \
   1052 		line_offset = st->args[i].line_offset;                                                \
   1053 		if (str_array_equal("mods", st->args[i].text, st->args[i].len)) {                     \
   1054 			if (mods_init) {                                                              \
   1055 				msg = "Duplicate mods specification";                                 \
   1056 				goto error;                                                           \
   1057 			} else if (i == st->len - 1) {                                                \
   1058 				msg = "Unfinished statement";                                         \
   1059 				goto error;                                                           \
   1060 			}                                                                             \
   1061 			i++;                                                                          \
   1062 			if (parse_modmask(st->args[i].text, st->args[i].len, &m->mods)) {             \
   1063 				msg = "Invalid mods specification";                                   \
   1064 				goto error;                                                           \
   1065 			}                                                                             \
   1066 			mods_init = 1;                                                                \
   1067 		} else if (str_array_equal("modes", st->args[i].text, st->args[i].len)) {             \
   1068 			if (modes_init) {                                                             \
   1069 				msg = "Duplicate modes specification";                                \
   1070 				goto error;                                                           \
   1071 			} else if (i == st->len - 1) {                                                \
   1072 				msg = "Unfinished statement";                                         \
   1073 				goto error;                                                           \
   1074 			}                                                                             \
   1075 			i++;                                                                          \
   1076 			if (mode_parse_func(st->args[i].text, st->args[i].len, &m->modes)) {          \
   1077 				msg = "Invalid modes specification";                                  \
   1078 				goto error;                                                           \
   1079 			} else if (!cb_type##_modemask_is_valid(m->cb, m->modes)) {                   \
   1080 				msg = "Function not defined for all given modes";                     \
   1081 				goto error;                                                           \
   1082 			}                                                                             \
   1083 			modes_init = 1;                                                               \
   1084 		} else if (str_array_equal("keysym", st->args[i].text, st->args[i].len)) {            \
   1085 			if (text_init) {                                                              \
   1086 				msg = "Text already specified";                                       \
   1087 				goto error;                                                           \
   1088 			} else if (keysym_init) {                                                     \
   1089 				msg = "Duplicate keysym specification";                               \
   1090 				goto error;                                                           \
   1091 			} else if (i == st->len - 1) {                                                \
   1092 				msg = "Unfinished statement";                                         \
   1093 				goto error;                                                           \
   1094 			}                                                                             \
   1095 			i++;                                                                          \
   1096 			if (parse_keysym(st->args[i].text, st->args[i].len, &m->keysym)) {            \
   1097 				msg = "Invalid keysym specification";                                 \
   1098 				goto error;                                                           \
   1099 			}                                                                             \
   1100 			keysym_init = 1;                                                              \
   1101 		} else if (str_array_equal("text", st->args[i].text, st->args[i].len)) {              \
   1102 			if (keysym_init) {                                                            \
   1103 				msg = "Keysym already specified";                                     \
   1104 				goto error;                                                           \
   1105 			} else if (text_init) {                                                       \
   1106 				msg = "Duplicate text specification";                                 \
   1107 				goto error;                                                           \
   1108 			} else if (i == st->len - 1) {                                                \
   1109 				msg = "Unfinished statement";                                         \
   1110 				goto error;                                                           \
   1111 			}                                                                             \
   1112 			i++;                                                                          \
   1113 			m->text = ledit_strndup(st->args[i].text, st->args[i].len);                   \
   1114 			text_init = 1;                                                                \
   1115 		} else if (str_array_equal("catchall", st->args[i].text, st->args[i].len)) {          \
   1116 			if (keysym_init) {                                                            \
   1117 				msg = "Keysym already specified";                                     \
   1118 				goto error;                                                           \
   1119 			} else if (text_init) {                                                       \
   1120 				msg = "Duplicate text specification";                                 \
   1121 				goto error;                                                           \
   1122 			}                                                                             \
   1123 			m->text = ledit_strdup("");                                                   \
   1124 			text_init = 1;                                                                \
   1125 		} else {                                                                              \
   1126 			msg = "Invalid statement";                                                    \
   1127 			goto error;                                                                   \
   1128 		}                                                                                     \
   1129 	}                                                                                             \
   1130 	if (!text_init && !keysym_init) {                                                             \
   1131 		msg = "No text or keysym specified";                                                  \
   1132 		goto error;                                                                           \
   1133 	}                                                                                             \
   1134 	if (!modes_init) {                                                                            \
   1135 		msg = "No modes specified";                                                           \
   1136 		goto error;                                                                           \
   1137 	}                                                                                             \
   1138 	return 0;                                                                                     \
   1139 error:                                                                                                \
   1140 	if (msg) {                                                                                    \
   1141 		*errstr = print_fmt(                                                                  \
   1142 		    "%s, line %zu, offset %zu: %s", filename, line, line_offset, msg                  \
   1143 		 );                                                                                   \
   1144 	}                                                                                             \
   1145 	if (m->text)                                                                                  \
   1146 		free(m->text);                                                                        \
   1147 	return 1;                                                                                     \
   1148 }
   1149 
   1150 GEN_PARSE_STATEMENT(parse_basic_key_statement, basic_key_cb, basic_key_mapping, parse_modemask)
   1151 GEN_PARSE_STATEMENT(parse_command_key_statement, command_key_cb, command_key_mapping, parse_command_modemask)
   1152 
   1153 static int
   1154 parse_command_statement(ast_statement *st, command_mapping *m, char *filename, char **errstr) {
   1155 	size_t line = st->func_tok.line;
   1156 	size_t line_offset = st->func_tok.line_offset;
   1157 	m->cb = NULL;
   1158 	m->text = NULL;
   1159 	char *msg = NULL;
   1160 	if (!str_array_equal("bind", st->func_tok.text, st->func_tok.len) || st->len != 2) {
   1161 		msg = "Invalid statement";
   1162 		goto error;
   1163 	}
   1164 	m->cb = command_cb_map_get_entry(st->args[0].text, st->args[0].len);
   1165 	if (!m->cb) {
   1166 		msg = "Invalid function specification";
   1167 		goto error;
   1168 	}
   1169 	m->text = ledit_strndup(st->args[1].text, st->args[1].len);
   1170 	return 0;
   1171 error:
   1172 	if (msg) {
   1173 		*errstr = print_fmt(
   1174 		    "%s, line %zu, offset %zu: %s", filename, line, line_offset, msg
   1175 		);
   1176 	}
   1177 	/* I guess this is unnecessary */
   1178 	if (m->text)
   1179 		free(m->text);
   1180 	return 1;
   1181 }
   1182 
   1183 static void
   1184 push_basic_key_mapping(basic_key_array *arr, basic_key_mapping m) {
   1185 	if (arr->num_keys == arr->alloc_keys) {
   1186 		arr->alloc_keys = ideal_array_size(arr->alloc_keys, add_sz(arr->num_keys, 1));
   1187 		arr->keys = ledit_reallocarray(arr->keys, arr->alloc_keys, sizeof(basic_key_mapping));
   1188 	}
   1189 	arr->keys[arr->num_keys] = m;
   1190 	arr->num_keys++;
   1191 }
   1192 
   1193 static void
   1194 push_command_key_mapping(command_key_array *arr, command_key_mapping m) {
   1195 	if (arr->num_keys == arr->alloc_keys) {
   1196 		arr->alloc_keys = ideal_array_size(arr->alloc_keys, add_sz(arr->num_keys, 1));
   1197 		arr->keys = ledit_reallocarray(arr->keys, arr->alloc_keys, sizeof(command_key_mapping));
   1198 	}
   1199 	arr->keys[arr->num_keys] = m;
   1200 	arr->num_keys++;
   1201 }
   1202 
   1203 static void
   1204 push_command_mapping(command_array *arr, command_mapping m) {
   1205 	if (arr->num_cmds == arr->alloc_cmds) {
   1206 		arr->alloc_cmds = ideal_array_size(arr->alloc_cmds, add_sz(arr->num_cmds, 1));
   1207 		arr->cmds = ledit_reallocarray(arr->cmds, arr->alloc_cmds, sizeof(command_mapping));
   1208 	}
   1209 	arr->cmds[arr->num_cmds] = m;
   1210 	arr->num_cmds++;
   1211 }
   1212 
   1213 /* FIXME: This could be made a lot nicer and less repetitive */
   1214 static int
   1215 load_bindings(struct config *cfg, ast_list *list, char *filename, char **errstr) {
   1216 	int basic_keys_init = 0, command_keys_init = 0, commands_init = 0;
   1217 	size_t cur_lang = cfg->num_langs - 1; /* FIXME: ensure no underflow */
   1218 	for (size_t i = 0; i < list->len; i++) {
   1219 		size_t line = list->objs[i].tok.line;
   1220 		size_t line_offset = list->objs[i].tok.line_offset;
   1221 		if (list->objs[i].type != OBJ_ASSIGNMENT) {
   1222 			*errstr = print_fmt(
   1223 			     "%s: Invalid statement in bindings configuration "
   1224 			     "at list %zu, offset %zu", filename, line, line_offset
   1225 			 );
   1226 			 goto error;
   1227 		}
   1228 		char *key = list->objs[i].obj.assignment.tok.text;
   1229 		size_t key_len = list->objs[i].obj.assignment.tok.len;
   1230 		if (str_array_equal("language", key, key_len)) {
   1231 			if (list->objs[i].obj.assignment.value->type != OBJ_STRING) {
   1232 				*errstr = print_fmt(
   1233 				    "%s: Invalid language setting in bindings configuration "
   1234 				    "at line %zu, offset %zu", filename, line, line_offset
   1235 				);
   1236 				goto error;
   1237 			} else if (cfg->langs[cur_lang]) {
   1238 				*errstr = print_fmt(
   1239 				    "%s: Duplicate language setting in bindings configuration "
   1240 				    "at line %zu, offset %zu", filename, line, line_offset
   1241 				);
   1242 				goto error;
   1243 			}
   1244 			char *val = list->objs[i].obj.assignment.value->obj.str.tok.text;
   1245 			size_t val_len = list->objs[i].obj.assignment.value->obj.str.tok.len;
   1246 			cfg->langs[cur_lang] = ledit_strndup(val, val_len);
   1247 		} else if (str_array_equal("basic-keys", key, key_len)) {
   1248 			if (list->objs[i].obj.assignment.value->type != OBJ_LIST) {
   1249 				*errstr = print_fmt(
   1250 				    "%s: Invalid basic-keys setting in bindings configuration "
   1251 				    "at line %zu, offset %zu", filename, line, line_offset
   1252 				);
   1253 				goto error;
   1254 			} else if (basic_keys_init) {
   1255 				*errstr = print_fmt(
   1256 				    "%s: Duplicate basic-keys setting in bindings configuration "
   1257 				    "at line %zu, offset %zu", filename, line, line_offset
   1258 				);
   1259 				goto error;
   1260 			}
   1261 			ast_list *slist = &list->objs[i].obj.assignment.value->obj.list;
   1262 			for (size_t j = 0; j < slist->len; j++) {
   1263 				line = slist->objs[j].tok.line;
   1264 				line_offset = slist->objs[j].tok.line_offset;
   1265 				if (slist->objs[j].type != OBJ_STATEMENT) {
   1266 					*errstr = print_fmt(
   1267 					    "%s: Invalid basic-keys setting in bindings configuration "
   1268 					    "at line %zu, offset %zu", filename, line, line_offset
   1269 					);
   1270 					goto error;
   1271 				}
   1272 				basic_key_mapping m;
   1273 				if (parse_basic_key_statement(&slist->objs[j].obj.statement, &m, filename, errstr))
   1274 					goto error;
   1275 				push_basic_key_mapping(&cfg->basic_keys[0], m);
   1276 			}
   1277 			basic_keys_init = 1;
   1278 		} else if (str_array_equal("command-keys", key, key_len)) {
   1279 			if (list->objs[i].obj.assignment.value->type != OBJ_LIST) {
   1280 				*errstr = print_fmt(
   1281 				    "%s: Invalid command-keys setting in bindings configuration "
   1282 				    "at line %zu, offset %zu", filename, line, line_offset
   1283 				);
   1284 				goto error;
   1285 			} else if (command_keys_init) {
   1286 				*errstr = print_fmt(
   1287 				    "%s: Duplicate command-keys setting in bindings configuration "
   1288 				    "at line %zu, offset %zu", filename, line, line_offset
   1289 				);
   1290 				goto error;
   1291 			}
   1292 			ast_list *slist = &list->objs[i].obj.assignment.value->obj.list;
   1293 			for (size_t j = 0; j < slist->len; j++) {
   1294 				line = slist->objs[j].tok.line;
   1295 				line_offset = slist->objs[j].tok.line_offset;
   1296 				if (slist->objs[j].type != OBJ_STATEMENT) {
   1297 					*errstr = print_fmt(
   1298 					    "%s: Invalid command-keys setting in bindings configuration "
   1299 					    "at line %zu, offset %zu", filename, line, line_offset
   1300 					);
   1301 					goto error;
   1302 				}
   1303 				command_key_mapping m;
   1304 				if (parse_command_key_statement(&slist->objs[j].obj.statement, &m, filename, errstr))
   1305 					goto error;
   1306 				push_command_key_mapping(&cfg->command_keys[0], m);
   1307 			}
   1308 			command_keys_init = 1;
   1309 		} else if (str_array_equal("commands", key, key_len)) {
   1310 			if (list->objs[i].obj.assignment.value->type != OBJ_LIST) {
   1311 				*errstr = print_fmt(
   1312 				    "%s: Invalid commands setting in bindings configuration "
   1313 				    "at line %zu, offset %zu", filename, line, line_offset
   1314 				);
   1315 				goto error;
   1316 			} else if (commands_init) {
   1317 				*errstr = print_fmt(
   1318 				    "%s: Duplicate commands setting in bindings configuration "
   1319 				    "at line %zu, offset %zu", filename, line, line_offset
   1320 				);
   1321 				goto error;
   1322 			}
   1323 			ast_list *slist = &list->objs[i].obj.assignment.value->obj.list;
   1324 			for (size_t j = 0; j < slist->len; j++) {
   1325 				line = slist->objs[j].tok.line;
   1326 				line_offset = slist->objs[j].tok.line_offset;
   1327 				if (slist->objs[j].type != OBJ_STATEMENT) {
   1328 					*errstr = print_fmt(
   1329 					    "%s: Invalid commands setting in bindings configuration "
   1330 					    "at line %zu, offset %zu", filename, line, line_offset
   1331 					);
   1332 					goto error;
   1333 				}
   1334 				command_mapping m;
   1335 				if (parse_command_statement(&slist->objs[j].obj.statement, &m, filename, errstr))
   1336 					goto error;
   1337 				push_command_mapping(&cfg->cmds[0], m);
   1338 			}
   1339 			commands_init = 1;
   1340 		}
   1341 	}
   1342 
   1343 	/* FIXME: the behavior here is a bit weird - if there is nothing other than a language
   1344 	   setting in the bindings configuration, all actual bindings are default, but the
   1345 	   associated language is different */
   1346 	if (!cfg->langs[cur_lang]) {
   1347 		cfg->langs[cur_lang] = ledit_strdup(language_default);
   1348 	}
   1349 	/* FIXME: avoid calling strlen */
   1350 	if (!basic_keys_init) {
   1351 		ledit_debug("No basic keys configured in bindings; loading defaults\n");
   1352 		basic_key_mapping m;
   1353 		for (size_t i = 0; i < LENGTH(basic_keys_default); i++) {
   1354 			m.cb = basic_key_cb_map_get_entry(basic_keys_default[i].func_name, strlen(basic_keys_default[i].func_name));
   1355 			if (!m.cb) {
   1356 				*errstr = print_fmt("default config: Invalid basic key function name '%s'", basic_keys_default[i].func_name);
   1357 				goto error;
   1358 			} else if (!basic_key_cb_modemask_is_valid(m.cb, basic_keys_default[i].modes)) {
   1359 				*errstr = print_fmt("default config: Function '%s' not defined for all given modes", basic_keys_default[i].func_name);
   1360 				goto error;
   1361 			}
   1362 			m.text = basic_keys_default[i].text ? ledit_strdup(basic_keys_default[i].text) : NULL;
   1363 			m.mods = basic_keys_default[i].mods;
   1364 			m.modes = basic_keys_default[i].modes;
   1365 			m.keysym = basic_keys_default[i].keysym;
   1366 			push_basic_key_mapping(&cfg->basic_keys[0], m);
   1367 		}
   1368 	}
   1369 	if (!command_keys_init) {
   1370 		ledit_debug("No command keys configured in bindings; loading defaults\n");
   1371 		command_key_mapping m;
   1372 		for (size_t i = 0; i < LENGTH(command_keys_default); i++) {
   1373 			m.cb = command_key_cb_map_get_entry(command_keys_default[i].func_name, strlen(command_keys_default[i].func_name));
   1374 			if (!m.cb) {
   1375 				*errstr = print_fmt("default config: Invalid command key function name '%s'", command_keys_default[i].func_name);
   1376 				goto error;
   1377 			} else if (!command_key_cb_modemask_is_valid(m.cb, command_keys_default[i].modes)) {
   1378 				*errstr = print_fmt("default config: Function '%s' not defined for all given modes", command_keys_default[i].func_name);
   1379 				goto error;
   1380 			}
   1381 			m.text = command_keys_default[i].text ? ledit_strdup(command_keys_default[i].text) : NULL;
   1382 			m.mods = command_keys_default[i].mods;
   1383 			m.modes = command_keys_default[i].modes;
   1384 			m.keysym = command_keys_default[i].keysym;
   1385 			push_command_key_mapping(&cfg->command_keys[0], m);
   1386 		}
   1387 	}
   1388 	/* FIXME: guard against NULL text in default config! */
   1389 	if (!commands_init) {
   1390 		ledit_debug("No commands configured in bindings; loading defaults\n");
   1391 		command_mapping m;
   1392 		for (size_t i = 0; i < LENGTH(commands_default); i++) {
   1393 			m.cb = command_cb_map_get_entry(commands_default[i].func_name, strlen(commands_default[i].func_name));
   1394 			if (!m.cb) {
   1395 				*errstr = print_fmt("default config: Invalid command function name '%s'", commands_default[i].func_name);
   1396 				goto error;
   1397 			}
   1398 			m.text = ledit_strdup(commands_default[i].text);
   1399 			push_command_mapping(&cfg->cmds[0], m);
   1400 		}
   1401 	}
   1402 	return 0;
   1403 /* FIXME: simplify error handling by doing more here */
   1404 error:
   1405 	return 1;
   1406 }
   1407 
   1408 static int
   1409 load_mapping(struct config *cfg, ast_list *list, char *filename, char **errstr) {
   1410 	int key_mapping_init = 0, command_mapping_init = 0;
   1411 	size_t cur_lang = cfg->num_langs - 1; /* FIXME: ensure no underflow */
   1412 	for (size_t i = 0; i < list->len; i++) {
   1413 		size_t line = list->objs[i].tok.line;
   1414 		size_t line_offset = list->objs[i].tok.line_offset;
   1415 		if (list->objs[i].type != OBJ_ASSIGNMENT) {
   1416 			*errstr = print_fmt(
   1417 			     "%s: Invalid statement in language mapping configuration "
   1418 			     "at list %zu, offset %zu", filename, line, line_offset
   1419 			 );
   1420 			 goto error;
   1421 		}
   1422 		char *key = list->objs[i].obj.assignment.tok.text;
   1423 		size_t key_len = list->objs[i].obj.assignment.tok.len;
   1424 		basic_key_array *bkmap = &cfg->basic_keys[cur_lang];
   1425 		command_key_array *ckmap = &cfg->command_keys[cur_lang];
   1426 		command_array *cmap = &cfg->cmds[cur_lang];
   1427 		if (str_array_equal("language", key, key_len)) {
   1428 			if (list->objs[i].obj.assignment.value->type != OBJ_STRING) {
   1429 				*errstr = print_fmt(
   1430 				    "%s: Invalid language setting in language mapping configuration "
   1431 				    "at line %zu, offset %zu", filename, line, line_offset
   1432 				);
   1433 				goto error;
   1434 			} else if (cfg->langs[cur_lang]) {
   1435 				*errstr = print_fmt(
   1436 				    "%s: Duplicate language setting in language mapping configuration "
   1437 				    "at line %zu, offset %zu", filename, line, line_offset
   1438 				);
   1439 				goto error;
   1440 			}
   1441 			char *val = list->objs[i].obj.assignment.value->obj.str.tok.text;
   1442 			size_t val_len = list->objs[i].obj.assignment.value->obj.str.tok.len;
   1443 			cfg->langs[cur_lang] = ledit_strndup(val, val_len);
   1444 		} else if (str_array_equal("key-mapping", key, key_len)) {
   1445 			if (list->objs[i].obj.assignment.value->type != OBJ_LIST) {
   1446 				*errstr = print_fmt(
   1447 				    "%s: Invalid key-mapping setting in language mapping configuration "
   1448 				    "at line %zu, offset %zu", filename, line, line_offset
   1449 				);
   1450 				goto error;
   1451 			} else if (key_mapping_init) {
   1452 				*errstr = print_fmt(
   1453 				    "%s: Duplicate key-mapping setting in language mapping configuration "
   1454 				    "at line %zu, offset %zu", filename, line, line_offset
   1455 				);
   1456 				goto error;
   1457 			}
   1458 			ast_list *slist = &list->objs[i].obj.assignment.value->obj.list;
   1459 			for (size_t j = 0; j < slist->len; j++) {
   1460 				line = slist->objs[j].tok.line;
   1461 				line_offset = slist->objs[j].tok.line_offset;
   1462 				if (slist->objs[j].type != OBJ_STATEMENT) {
   1463 					*errstr = print_fmt(
   1464 					    "%s: Invalid key-mapping setting in language mapping configuration "
   1465 					    "at line %zu, offset %zu", filename, line, line_offset
   1466 					);
   1467 					goto error;
   1468 				}
   1469 				ast_statement *st = &slist->objs[j].obj.statement;
   1470 				if (!str_array_equal("map", st->func_tok.text, st->func_tok.len) || st->len != 2) {
   1471 					*errstr = print_fmt(
   1472 					    "%s: Invalid key-mapping statement in language mapping configuration "
   1473 					    "at line %zu, offset %zu", filename, line, line_offset
   1474 					);
   1475 					goto error;
   1476 				}
   1477 				/* FIXME: any way to speed this up? I guess once the keys can be binary-searched... */
   1478 				for (size_t k = 0; k < bkmap->num_keys; k++) {
   1479 					if (bkmap->keys[k].text && str_array_equal(bkmap->keys[k].text, st->args[1].text, st->args[1].len)) {
   1480 						free(bkmap->keys[k].text);
   1481 						bkmap->keys[k].text = ledit_strndup(st->args[0].text, st->args[0].len);
   1482 					}
   1483 				}
   1484 				for (size_t k = 0; k < ckmap->num_keys; k++) {
   1485 					if (ckmap->keys[k].text && str_array_equal(ckmap->keys[k].text, st->args[1].text, st->args[1].len)) {
   1486 						free(ckmap->keys[k].text);
   1487 						ckmap->keys[k].text = ledit_strndup(st->args[0].text, st->args[0].len);
   1488 					}
   1489 				}
   1490 			}
   1491 			key_mapping_init = 1;
   1492 		} else if (str_array_equal("command-mapping", key, key_len)) {
   1493 			if (list->objs[i].obj.assignment.value->type != OBJ_LIST) {
   1494 				*errstr = print_fmt(
   1495 				    "%s: Invalid command-mapping setting in language mapping configuration "
   1496 				    "at line %zu, offset %zu", filename, line, line_offset
   1497 				);
   1498 				goto error;
   1499 			} else if (command_mapping_init) {
   1500 				*errstr = print_fmt(
   1501 				    "%s: Duplicate command-mapping setting in language mapping configuration "
   1502 				    "at line %zu, offset %zu", filename, line, line_offset
   1503 				);
   1504 				goto error;
   1505 			}
   1506 			ast_list *slist = &list->objs[i].obj.assignment.value->obj.list;
   1507 			for (size_t j = 0; j < slist->len; j++) {
   1508 				line = slist->objs[j].tok.line;
   1509 				line_offset = slist->objs[j].tok.line_offset;
   1510 				if (slist->objs[j].type != OBJ_STATEMENT) {
   1511 					*errstr = print_fmt(
   1512 					    "%s: Invalid command-mapping setting in language mapping configuration "
   1513 					    "at line %zu, offset %zu", filename, line, line_offset
   1514 					);
   1515 					goto error;
   1516 				}
   1517 				ast_statement *st = &slist->objs[j].obj.statement;
   1518 				if (!str_array_equal("map", st->func_tok.text, st->func_tok.len) || st->len != 2) {
   1519 					*errstr = print_fmt(
   1520 					    "%s: Invalid command-mapping statement in language mapping configuration "
   1521 					    "at line %zu, offset %zu", filename, line, line_offset
   1522 					);
   1523 					goto error;
   1524 				}
   1525 				for (size_t k = 0; k < cmap->num_cmds; k++) {
   1526 					if (str_array_equal(cmap->cmds[k].text, st->args[1].text, st->args[1].len)) {
   1527 						free(cmap->cmds[k].text);
   1528 						cmap->cmds[k].text = ledit_strndup(st->args[0].text, st->args[0].len);
   1529 					}
   1530 				}
   1531 			}
   1532 			command_mapping_init = 1;
   1533 		}
   1534 	}
   1535 	if (!cfg->langs[cur_lang]) {
   1536 		/* FIXME: pass actual beginning line and offset so this doesn't have to
   1537 		   use the line and offset of the first list element */
   1538 		if (list->len > 0) {
   1539 			*errstr = print_fmt(
   1540 			    "%s: Missing language setting in language mapping configuration "
   1541 			    "at line %zu, offset %zu", filename, list->objs[0].tok.line, list->objs[0].tok.line_offset
   1542 			);
   1543 		} else {
   1544 			*errstr = print_fmt("%s: Missing language setting in language mapping configuration", filename);
   1545 		}
   1546 		goto error;
   1547 	}
   1548 	return 0;
   1549 error:
   1550 	return 1;
   1551 }
   1552 
   1553 static void
   1554 append_mapping(struct config *cfg) {
   1555 	push_lang(cfg);
   1556 	ledit_assert(cfg->num_langs > 1);
   1557 
   1558 	/* first duplicate original mappings before replacing the text */
   1559 	/* FIXME: optimize this to avoid useless reallocations */
   1560 	size_t cur_lang = cfg->num_langs - 1;
   1561 	basic_key_array *arr1 = &cfg->basic_keys[cur_lang];
   1562 	arr1->num_keys = arr1->alloc_keys = cfg->basic_keys[0].num_keys;
   1563 	arr1->keys = ledit_reallocarray(NULL, arr1->num_keys, sizeof(basic_key_mapping));
   1564 	memmove(arr1->keys, cfg->basic_keys[0].keys, arr1->num_keys * sizeof(basic_key_mapping));
   1565 	for (size_t i = 0; i < arr1->num_keys; i++) {
   1566 		if (arr1->keys[i].text)
   1567 			arr1->keys[i].text = ledit_strdup(arr1->keys[i].text);
   1568 	}
   1569 
   1570 
   1571 	command_key_array *arr2 = &cfg->command_keys[cur_lang];
   1572 	arr2->num_keys = arr2->alloc_keys = cfg->command_keys[0].num_keys;
   1573 	arr2->keys = ledit_reallocarray(NULL, arr2->num_keys, sizeof(command_key_mapping));
   1574 	memmove(arr2->keys, cfg->command_keys[0].keys, arr2->num_keys * sizeof(command_key_mapping));
   1575 	for (size_t i = 0; i < arr2->num_keys; i++) {
   1576 		if (arr2->keys[i].text)
   1577 			arr2->keys[i].text = ledit_strdup(arr2->keys[i].text);
   1578 	}
   1579 
   1580 	command_array *arr3 = &cfg->cmds[cur_lang];
   1581 	arr3->num_cmds = arr3->alloc_cmds = cfg->cmds[0].num_cmds;
   1582 	arr3->cmds = ledit_reallocarray(NULL, arr3->num_cmds, sizeof(command_mapping));
   1583 	memmove(arr3->cmds, cfg->cmds[0].cmds, arr3->num_cmds * sizeof(command_mapping));
   1584 	for (size_t i = 0; i < arr3->num_cmds; i++) {
   1585 		arr3->cmds[i].text = ledit_strdup(arr3->cmds[i].text);
   1586 	}
   1587 }
   1588 
   1589 #ifdef LEDIT_DEBUG
   1590 static void
   1591 debug_print_obj(ast_obj *obj, int shiftwidth) {
   1592 	for (int i = 0; i < shiftwidth; i++) {
   1593 		fprintf(stderr, " ");
   1594 	}
   1595 	switch (obj->type) {
   1596 	case OBJ_STRING:
   1597 		fprintf(stderr, "STRING: %.*s\n", (int)obj->obj.str.tok.len, obj->obj.str.tok.text);
   1598 		break;
   1599 	case OBJ_STATEMENT:
   1600 		fprintf(stderr, "STATEMENT: %.*s ", (int)obj->obj.statement.func_tok.len, obj->obj.statement.func_tok.text);
   1601 		for (size_t i = 0; i < obj->obj.statement.len; i++) {
   1602 			fprintf(stderr, "%.*s ", (int)obj->obj.statement.args[i].len, obj->obj.statement.args[i].text);
   1603 		}
   1604 		fprintf(stderr, "\n");
   1605 		break;
   1606 	case OBJ_ASSIGNMENT:
   1607 		fprintf(stderr, "ASSIGNMENT: %.*s =\n", (int)obj->obj.assignment.tok.len, obj->obj.assignment.tok.text);
   1608 		debug_print_obj(obj->obj.assignment.value, shiftwidth + 4);
   1609 		break;
   1610 	case OBJ_LIST:
   1611 		fprintf(stderr, "LIST:\n");
   1612 		for (size_t i = 0; i < obj->obj.list.len; i++) {
   1613 			debug_print_obj(&obj->obj.list.objs[i], shiftwidth + 4);
   1614 		}
   1615 		break;
   1616 	}
   1617 }
   1618 #endif
   1619 
   1620 /* WARNING: *errstr must be freed! */
   1621 int
   1622 config_loadfile(ledit_common *common, char *filename, char **errstr) {
   1623 	#ifdef LEDIT_DEBUG
   1624 	struct timespec now, elapsed, last;
   1625 	clock_gettime(CLOCK_MONOTONIC, &last);
   1626 	#endif
   1627 	size_t len;
   1628 	*errstr = NULL;
   1629 	ast_list list = {.objs = NULL, .len = 0, .cap = 0};
   1630 	char *file_contents = NULL;
   1631 	if (filename) {
   1632 		file_contents = load_file(filename, &len, errstr);
   1633 		if (!file_contents) return 1;
   1634 		#ifdef LEDIT_DEBUG
   1635 		clock_gettime(CLOCK_MONOTONIC, &now);
   1636 		ledit_timespecsub(&now, &last, &elapsed);
   1637 		ledit_debug_fmt(
   1638 		    "Time to load config file: %lld seconds, %ld nanoseconds\n",
   1639 		    (long long)elapsed.tv_sec, elapsed.tv_nsec
   1640 		);
   1641 		last = now;
   1642 		#endif
   1643 		/* start at line 1 to make error messages more useful */
   1644 		struct lexstate s = {file_contents, len, 0, 1, 0};
   1645 		if (parse_list(&s, &list, 1, filename, errstr)) {
   1646 			free(file_contents);
   1647 			return 1;
   1648 		}
   1649 		#ifdef LEDIT_DEBUG
   1650 		clock_gettime(CLOCK_MONOTONIC, &now);
   1651 		ledit_timespecsub(&now, &last, &elapsed);
   1652 		ledit_debug_fmt(
   1653 		    "Time to parse config file: %lld seconds, %ld nanoseconds\n",
   1654 		    (long long)elapsed.tv_sec, elapsed.tv_nsec
   1655 		);
   1656 		#endif
   1657 	}
   1658 
   1659 	#ifdef LEDIT_DEBUG
   1660 	clock_gettime(CLOCK_MONOTONIC, &last);
   1661 	for (size_t i = 0; i < list.len; i++) {
   1662 		debug_print_obj(&list.objs[i], 0);
   1663 	}
   1664 	clock_gettime(CLOCK_MONOTONIC, &now);
   1665 	ledit_timespecsub(&now, &last, &elapsed);
   1666 	ledit_debug_fmt(
   1667 	    "Time to print useless information: %lld seconds, %ld nanoseconds\n",
   1668 	    (long long)elapsed.tv_sec, elapsed.tv_nsec
   1669 	);
   1670 	clock_gettime(CLOCK_MONOTONIC, &last);
   1671 	#endif
   1672 
   1673 	struct config cfg = {NULL, NULL, NULL, NULL, NULL, 0, 0};
   1674 	int theme_init = 0, bindings_init = 0, mappings_init = 0;
   1675 	ast_assignment *assignment;
   1676 	for (size_t i = 0; i < list.len; i++) {
   1677 		switch (list.objs[i].type) {
   1678 		case OBJ_ASSIGNMENT:
   1679 			assignment = &list.objs[i].obj.assignment;
   1680 			if (str_array_equal("theme", assignment->tok.text, assignment->tok.len)) {
   1681 				if (theme_init) {
   1682 					*errstr = print_fmt(
   1683 					    "%s: Duplicate theme definition at line %zu, offset %zu",
   1684 					    filename, assignment->tok.line, assignment->tok.line_offset
   1685 					);
   1686 					goto error;
   1687 				} else if (assignment->value->type != OBJ_LIST) {
   1688 					*errstr = print_fmt(
   1689 					    "%s: Invalid theme definition at line %zu, offset %zu",
   1690 					    filename, assignment->tok.line, assignment->tok.line_offset
   1691 					);
   1692 					goto error;
   1693 				}
   1694 				cfg.theme = load_theme(common, &assignment->value->obj.list, filename, errstr);
   1695 				if (!cfg.theme)
   1696 					goto error;
   1697 				theme_init = 1;
   1698 			} else if (str_array_equal("bindings", assignment->tok.text, assignment->tok.len)) {
   1699 				if (bindings_init) {
   1700 					*errstr = print_fmt(
   1701 					    "%s: Duplicate definition of bindings at line %zu, offset %zu",
   1702 					    filename, assignment->tok.line, assignment->tok.line_offset
   1703 					);
   1704 					goto error;
   1705 				}
   1706 				push_lang(&cfg);
   1707 				if (assignment->value->type != OBJ_LIST) {
   1708 					*errstr = print_fmt(
   1709 					    "%s: Invalid definition of bindings at line %zu, offset %zu",
   1710 					    filename, assignment->tok.line, assignment->tok.line_offset
   1711 					);
   1712 					goto error;
   1713 				}
   1714 				if (load_bindings(&cfg, &assignment->value->obj.list, filename, errstr))
   1715 					goto error;
   1716 				bindings_init = 1;
   1717 			} else if (str_array_equal("language-mapping", assignment->tok.text, assignment->tok.len)) {
   1718 				if (cfg.num_langs == 0) {
   1719 					ledit_debug("No key/command bindings configured; loading defaults\n");
   1720 					push_lang(&cfg);
   1721 					/* load default config */
   1722 					ast_list empty_list = {.objs = NULL, .len = 0, .cap = 0};
   1723 					/* shouldn't usually happen */
   1724 					if (load_bindings(&cfg, &empty_list, filename, errstr))
   1725 						goto error;
   1726 					bindings_init = 1;
   1727 				} else if (assignment->value->type != OBJ_LIST) {
   1728 					*errstr = print_fmt(
   1729 					    "%s: Invalid definition of language mapping at line %zu, offset %zu",
   1730 					    filename, assignment->tok.line, assignment->tok.line_offset
   1731 					);
   1732 					goto error;
   1733 				}
   1734 
   1735 				append_mapping(&cfg);
   1736 
   1737 				if (load_mapping(&cfg, &assignment->value->obj.list, filename, errstr))
   1738 					goto error;
   1739 				mappings_init = 1;
   1740 			} else {
   1741 				*errstr = print_fmt(
   1742 				    "%s: Invalid assignment at line %zu, offset %zu",
   1743 				    filename, assignment->tok.line, assignment->tok.line_offset
   1744 				);
   1745 				goto error;
   1746 			}
   1747 			break;
   1748 		default:
   1749 			*errstr = print_fmt(
   1750 			    "%s: Invalid statement at line %zu, offset %zu",
   1751 			    filename, list.objs[i].tok.line, list.objs[i].tok.line_offset
   1752 			);
   1753 			goto error;
   1754 		}
   1755 	}
   1756 	if (!theme_init) {
   1757 		ledit_debug("No theme configured; loading defaults\n");
   1758 		cfg.theme = load_theme(common, NULL, NULL, errstr);
   1759 		if (!cfg.theme)
   1760 			goto error;
   1761 	}
   1762 	if (!bindings_init) {
   1763 		ledit_debug("No key/command bindings configured; loading defaults\n");
   1764 		push_lang(&cfg);
   1765 		/* load default config */
   1766 		ast_list empty_list = {.objs = NULL, .len = 0, .cap = 0};
   1767 		/* shouldn't usually happen */
   1768 		if (load_bindings(&cfg, &empty_list, NULL, errstr))
   1769 			goto error;
   1770 	}
   1771 	if (!mappings_init) {
   1772 		ledit_debug("No key/command mappings configured; loading defaults\n");
   1773 		for (size_t i = 0; i < LENGTH(mappings_default); i++) {
   1774 			append_mapping(&cfg);
   1775 			size_t cur_lang = cfg.num_langs - 1;
   1776 			cfg.langs[cur_lang] = ledit_strdup(mappings_default[i].lang);
   1777 			basic_key_array *bkmap = &cfg.basic_keys[cur_lang];
   1778 			command_key_array *ckmap = &cfg.command_keys[cur_lang];
   1779 			command_array *cmap = &cfg.cmds[cur_lang];
   1780 			/* FIXME: any way to speed this up? I guess once the keys can be binary-searched... */
   1781 			/* FIXME: duplicated code from above */
   1782 			for (size_t j = 0; j < mappings_default[i].keys_len; j++) {
   1783 				for (size_t k = 0; k < bkmap->num_keys; k++) {
   1784 					if (bkmap->keys[k].text && !strcmp(bkmap->keys[k].text, mappings_default[i].keys[j].to)) {
   1785 						free(bkmap->keys[k].text);
   1786 						bkmap->keys[k].text = ledit_strdup(mappings_default[i].keys[j].from);
   1787 					}
   1788 				}
   1789 				for (size_t k = 0; k < ckmap->num_keys; k++) {
   1790 					if (ckmap->keys[k].text && !strcmp(ckmap->keys[k].text, mappings_default[i].keys[j].to)) {
   1791 						free(ckmap->keys[k].text);
   1792 						ckmap->keys[k].text = ledit_strdup(mappings_default[i].keys[j].from);
   1793 					}
   1794 				}
   1795 			}
   1796 			for (size_t j = 0; j < mappings_default[i].cmds_len; j++) {
   1797 				for (size_t k = 0; k < cmap->num_cmds; k++) {
   1798 					if (!strcmp(cmap->cmds[k].text, mappings_default[i].keys[j].to)) {
   1799 						free(cmap->cmds[k].text);
   1800 						cmap->cmds[k].text = ledit_strdup(mappings_default[i].keys[j].from);
   1801 					}
   1802 				}
   1803 			}
   1804 		}
   1805 	}
   1806 	destroy_list(&list);
   1807 	free(file_contents);
   1808 	config_destroy(common, &config);
   1809 	config = cfg;
   1810 	#ifdef LEDIT_DEBUG
   1811 	clock_gettime(CLOCK_MONOTONIC, &now);
   1812 	ledit_timespecsub(&now, &last, &elapsed);
   1813 	ledit_debug_fmt(
   1814 	    "Time to interpret config file: %lld seconds, %ld nanoseconds\n",
   1815 	    (long long)elapsed.tv_sec, elapsed.tv_nsec
   1816 	);
   1817 	#endif
   1818 	return 0;
   1819 error:
   1820 	destroy_list(&list);
   1821 	free(file_contents);
   1822 	config_destroy(common, &cfg);
   1823 	return 1;
   1824 }
   1825 
   1826 ledit_theme *
   1827 config_get_theme(void) {
   1828 	ledit_assert(config.theme != NULL);
   1829 	return config.theme;
   1830 }
   1831 
   1832 basic_key_array *
   1833 config_get_basic_keys(size_t lang_index) {
   1834 	ledit_assert(lang_index < config.num_langs);
   1835 	return &config.basic_keys[lang_index];
   1836 }
   1837 
   1838 command_key_array *
   1839 config_get_command_keys(size_t lang_index) {
   1840 	ledit_assert(lang_index < config.num_langs);
   1841 	return &config.command_keys[lang_index];
   1842 }
   1843 
   1844 command_array *
   1845 config_get_commands(size_t lang_index) {
   1846 	ledit_assert(lang_index < config.num_langs);
   1847 	return &config.cmds[lang_index];
   1848 }
   1849 
   1850 int
   1851 config_get_language_index(char *lang, size_t *idx_ret) {
   1852 	for (size_t i = 0; i < config.num_langs; i++) {
   1853 		if (!strcmp(lang, config.langs[i])) {
   1854 			*idx_ret = i;
   1855 			return 0;
   1856 		}
   1857 	}
   1858 	return 1;
   1859 }
   1860 
   1861 char *
   1862 config_get_language_string(size_t lang_index) {
   1863 	if (lang_index >= config.num_langs)
   1864 		return NULL;
   1865 	return config.langs[lang_index];
   1866 }