configparser.c - ledit - Text editor (WIP)
HTML git clone git://lumidify.org/ledit.git (fast, but not encrypted)
HTML git clone https://lumidify.org/ledit.git (encrypted, but very slow)
HTML git clone git://4kcetb7mo7hj6grozzybxtotsub5bempzo4lirzc3437amof2c2impyd.onion/ledit.git (over tor)
DIR Log
DIR Files
DIR Refs
DIR README
DIR 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 }