// Read a configuration file. // Config files are formatted as several sections, each containing key = value pairs. // e.g.: // [section1] // thing1 = 33 // thing2 = 454 // [section2] // asdf = 123 // all worth it for the -Wformat warnings #define config_err(cfg, ...) do {\ if ((cfg)->error) break;\ snprintf((cfg)->ted->error, sizeof (cfg)->ted->error - 1, "%s:%u: ", (cfg)->filename, (cfg)->line_number), \ snprintf((cfg)->ted->error + strlen((cfg)->ted->error), sizeof (cfg)->ted->error - 1 - strlen((cfg)->ted->error), __VA_ARGS__), \ (cfg)->error = true; } while (0) typedef struct { Ted *ted; char const *filename; u32 line_number; // currently processing this line number bool error; } ConfigReader; static void context_copy(SettingsContext *dest, const SettingsContext *src) { *dest = *src; if (src->path) dest->path = str_dup(src->path); } static bool context_is_parent(const SettingsContext *parent, const SettingsContext *child) { if (child->language == 0 && parent->language != 0) return false; if (parent->language != 0 && child->language != 0 && parent->language != child->language) return false; if (parent->path) { if (!child->path) return false; if (!str_is_prefix(parent->path, child->path)) return false; } return true; } static void settings_copy(Settings *dest, const Settings *src) { *dest = *src; context_copy(&dest->context, &src->context); for (u32 i = 0; i < LANG_COUNT; ++i) { if (src->language_extensions[i]) dest->language_extensions[i] = str_dup(src->language_extensions[i]); } } static void context_free(SettingsContext *ctx) { free(ctx->path); memset(ctx, 0, sizeof *ctx); } static void settings_free(Settings *settings) { context_free(&settings->context); for (u32 i = 0; i < LANG_COUNT; ++i) free(settings->language_extensions[i]); memset(settings, 0, sizeof *settings); } static void config_part_free(ConfigPart *part) { context_free(&part->context); arr_clear(part->text); free(part->file); memset(part, 0, sizeof *part); } // Returns the key combination described by str. static u32 config_parse_key_combo(ConfigReader *cfg, char const *str) { u32 modifier = 0; // read modifier while (true) { if (str_is_prefix(str, "Ctrl+")) { if (modifier & KEY_MODIFIER_CTRL) { config_err(cfg, "Ctrl+ written twice"); return 0; } modifier |= KEY_MODIFIER_CTRL; str += strlen("Ctrl+"); } else if (str_is_prefix(str, "Shift+")) { if (modifier & KEY_MODIFIER_SHIFT) { config_err(cfg, "Shift+ written twice"); return 0; } modifier |= KEY_MODIFIER_SHIFT; str += strlen("Shift+"); } else if (str_is_prefix(str, "Alt+")) { if (modifier & KEY_MODIFIER_ALT) { config_err(cfg, "Alt+ written twice"); return 0; } modifier |= KEY_MODIFIER_ALT; str += strlen("Alt+"); } else break; } // read key SDL_Scancode scancode = SDL_GetScancodeFromName(str); if (scancode == SDL_SCANCODE_UNKNOWN) { typedef struct { char const *keyname1; char const *keyname2; // alternate key name SDL_Scancode scancode; bool shift; } KeyName; static KeyName const key_names[] = { {"Apostrophe", "Single Quote", SDL_SCANCODE_APOSTROPHE, 0}, {"Backslash", 0, SDL_SCANCODE_BACKSLASH, 0}, {"Comma", 0, SDL_SCANCODE_COMMA, 0}, {"Equals", 0, SDL_SCANCODE_EQUALS, 0}, {"Grave", "Backtick", SDL_SCANCODE_GRAVE, 0}, {"Keypad Plus", 0, SDL_SCANCODE_KP_PLUS, 0}, {"Keypad Minus", 0, SDL_SCANCODE_KP_MINUS, 0}, {"Keypad Divide", 0, SDL_SCANCODE_KP_DIVIDE, 0}, {"Keypad Multiply", 0, SDL_SCANCODE_KP_MULTIPLY, 0}, {"Left Bracket", 0, SDL_SCANCODE_LEFTBRACKET, 0}, // [ {"Right Bracket", 0, SDL_SCANCODE_RIGHTBRACKET, 0}, // ] {"Dash", 0, SDL_SCANCODE_MINUS, 0}, {"Minus", 0, SDL_SCANCODE_MINUS, 0}, {"Period", 0, SDL_SCANCODE_PERIOD, 0}, {"Semicolon", 0, SDL_SCANCODE_SEMICOLON, 0}, {"Slash", 0, SDL_SCANCODE_SLASH, 0}, {"Enter", 0, SDL_SCANCODE_RETURN, 0}, {"Keypad Return", 0, SDL_SCANCODE_KP_ENTER, 0}, {"Exclaim", "Exclamation Mark", SDL_SCANCODE_1, 1}, {"!", 0, SDL_SCANCODE_1, 1}, {"At", "@", SDL_SCANCODE_2, 1}, {"Hash", "#", SDL_SCANCODE_3, 1}, {"Dollar", "$", SDL_SCANCODE_4, 1}, {"Percent", "%", SDL_SCANCODE_5, 1}, {"Caret", "^", SDL_SCANCODE_6, 1}, {"Ampersand", "&", SDL_SCANCODE_7, 1}, {"Asterisk", "*", SDL_SCANCODE_8, 1}, {"Left Paren", "(", SDL_SCANCODE_9, 1}, {"Right Paren", ")", SDL_SCANCODE_0, 1}, {"Underscore", "_", SDL_SCANCODE_MINUS, 1}, {"Plus", "+", SDL_SCANCODE_EQUALS, 1}, {"Left Brace", "{", SDL_SCANCODE_LEFTBRACKET, 1}, {"Right Brace", "}", SDL_SCANCODE_RIGHTBRACKET, 1}, {"Pipe", "|", SDL_SCANCODE_BACKSLASH, 1}, {"Colon", ":", SDL_SCANCODE_SEMICOLON, 1}, {"Double Quote", "\"", SDL_SCANCODE_APOSTROPHE, 1}, {"Less Than", "<", SDL_SCANCODE_COMMA, 1}, {"Greater Than", ">", SDL_SCANCODE_PERIOD, 1}, {"Question Mark", "?", SDL_SCANCODE_SLASH, 1}, {"Question", 0, SDL_SCANCODE_SLASH, 1}, {"Tilde", "~", SDL_SCANCODE_GRAVE, 1}, {"X1", "x1", SCANCODE_MOUSE_X1, 0}, {"X2", "x2", SCANCODE_MOUSE_X2, 0} }; // @OPTIMIZE: sort key_names (and split keyname1/2); do a binary search for (size_t i = 0; i < arr_count(key_names); ++i) { KeyName const *k = &key_names[i]; if (streq(str, k->keyname1) || (k->keyname2 && streq(str, k->keyname2))) { scancode = k->scancode; if (k->shift) { if (modifier & KEY_MODIFIER_SHIFT) { config_err(cfg, "Shift+%s is redundant.", str); return 0; } modifier |= KEY_MODIFIER_SHIFT; } break; } } if (scancode == SDL_SCANCODE_UNKNOWN) { if (isdigit(str[0])) { // direct scancode numbers, e.g. Ctrl+24 or Ctrl+08 char *endp; long n = strtol(str, &endp, 10); if (*endp == '\0' && n > 0 && n < SCANCODE_COUNT) { scancode = (SDL_Scancode)n; } else { config_err(cfg, "Invalid scancode number: %s", str); return 0; } } else { config_err(cfg, "Unrecognized key name: %s.", str); return 0; } } } return (u32)scancode << 3 | modifier; } // all the "control" pointers here are relative to a NULL Settings object. typedef struct { char const *name; const bool *control; bool per_language; // allow per-language control } OptionBool; typedef struct { char const *name; const u8 *control; u8 min, max; bool per_language; } OptionU8; typedef struct { char const *name; const float *control; float min, max; bool per_language; } OptionFloat; typedef struct { char const *name; const u16 *control; u16 min, max; bool per_language; } OptionU16; typedef struct { char const *name; const char *control; size_t buf_size; bool per_language; } OptionString; typedef enum { OPTION_BOOL = 1, OPTION_U8, OPTION_U16, OPTION_FLOAT, OPTION_STRING } OptionType; typedef struct { OptionType type; const char *name; bool per_language; union { OptionU8 _u8; OptionBool _bool; OptionU16 _u16; OptionFloat _float; OptionString _string; } u; } OptionAny; // core options static Settings const options_zero = {0}; static OptionBool const options_bool[] = { {"auto-indent", &options_zero.auto_indent, true}, {"auto-add-newline", &options_zero.auto_add_newline, true}, {"auto-reload", &options_zero.auto_reload, true}, {"auto-reload-config", &options_zero.auto_reload_config, false}, {"syntax-highlighting", &options_zero.syntax_highlighting, true}, {"line-numbers", &options_zero.line_numbers, true}, {"restore-session", &options_zero.restore_session, false}, {"regenerate-tags-if-not-found", &options_zero.regenerate_tags_if_not_found, true}, {"indent-with-spaces", &options_zero.indent_with_spaces, true}, }; static OptionU8 const options_u8[] = { {"tab-width", &options_zero.tab_width, 1, 100, true}, {"cursor-width", &options_zero.cursor_width, 1, 100, true}, {"undo-save-time", &options_zero.undo_save_time, 1, 200, true}, {"border-thickness", &options_zero.border_thickness, 1, 30, false}, {"padding", &options_zero.padding, 0, 100, false}, {"scrolloff", &options_zero.scrolloff, 1, 100, true}, {"tags-max-depth", &options_zero.tags_max_depth, 1, 100, false}, }; static OptionU16 const options_u16[] = { {"text-size", &options_zero.text_size, TEXT_SIZE_MIN, TEXT_SIZE_MAX, true}, {"max-menu-width", &options_zero.max_menu_width, 10, U16_MAX, false}, {"error-display-time", &options_zero.error_display_time, 0, U16_MAX, false}, }; static OptionFloat const options_float[] = { {"cursor-blink-time-on", &options_zero.cursor_blink_time_on, 0, 1000, true}, {"cursor-blink-time-off", &options_zero.cursor_blink_time_off, 0, 1000, true}, }; static OptionString const options_string[] = { {"build-default-command", options_zero.build_default_command, sizeof options_zero.build_default_command, true}, }; static void option_bool_set(Settings *settings, const OptionBool *opt, bool value) { *(bool *)((char *)settings + ((char*)opt->control - (char*)&options_zero)) = value; } static void option_u8_set(Settings *settings, const OptionU8 *opt, u8 value) { if (value >= opt->min && value <= opt->max) *(u8 *)((char *)settings + ((char*)opt->control - (char*)&options_zero)) = value; } static void option_u16_set(Settings *settings, const OptionU16 *opt, u16 value) { if (value >= opt->min && value <= opt->max) *(u16 *)((char *)settings + ((char*)opt->control - (char*)&options_zero)) = value; } static void option_float_set(Settings *settings, const OptionFloat *opt, float value) { if (value >= opt->min && value <= opt->max) *(float *)((char *)settings + ((char*)opt->control - (char*)&options_zero)) = value; } static void option_string_set(Settings *settings, const OptionString *opt, const char *value) { char *control = (char *)settings + (opt->control - (char*)&options_zero); str_cpy(control, opt->buf_size, value); } static void parse_section_header(ConfigReader *cfg, char *line, ConfigPart *part) { #define SECTION_HEADER_HELP "Section headers should look like this: [(path//)(language.)section-name]" char *closing = strchr(line, ']'); if (!closing) { config_err(cfg, "Unmatched [. " SECTION_HEADER_HELP); return; } else if (closing[1] != '\0') { config_err(cfg, "Text after section. " SECTION_HEADER_HELP); return; } else { *closing = '\0'; char *section = line + 1; char *path_end = strstr(section, "//"); if (path_end) { size_t path_len = (size_t)(path_end - section); // @TODO: expand ~ part->context.path = strn_dup(section, path_len); section = path_end + 2; } char *dot = strchr(section, '.'); if (dot) { *dot = '\0'; Language language = part->context.language = language_from_str(section); if (!language) { config_err(cfg, "Unrecognized language: %s.", section); return; } section = dot + 1; } if (streq(section, "keyboard")) { part->section = SECTION_KEYBOARD; } else if (streq(section, "colors")) { part->section = SECTION_COLORS; } else if (streq(section, "core")) { part->section = SECTION_CORE; } else if (streq(section, "extensions")) { if (part->context.language != 0 || part->context.path) { config_err(cfg, "Extensions section cannot be language- or path-specific."); return; } part->section = SECTION_EXTENSIONS; } else { config_err(cfg, "Unrecognized section: [%s].", section); return; } } } static bool initialized_options = false; static OptionAny all_options[1000] = {0}; static void config_init_options(void) { if (initialized_options) return; OptionAny *opt = all_options; for (size_t i = 0; i < arr_count(options_bool); ++i) { opt->type = OPTION_BOOL; opt->name = options_bool[i].name; opt->per_language = options_bool[i].per_language; opt->u._bool = options_bool[i]; ++opt; } for (size_t i = 0; i < arr_count(options_u8); ++i) { opt->type = OPTION_U8; opt->name = options_u8[i].name; opt->per_language = options_u8[i].per_language; opt->u._u8 = options_u8[i]; ++opt; } for (size_t i = 0; i < arr_count(options_u16); ++i) { opt->type = OPTION_U16; opt->name = options_u16[i].name; opt->per_language = options_u16[i].per_language; opt->u._u16 = options_u16[i]; ++opt; } for (size_t i = 0; i < arr_count(options_float); ++i) { opt->type = OPTION_FLOAT; opt->name = options_float[i].name; opt->per_language = options_float[i].per_language; opt->u._float = options_float[i]; ++opt; } for (size_t i = 0; i < arr_count(options_string); ++i) { opt->type = OPTION_STRING; opt->name = options_string[i].name; opt->per_language = options_string[i].per_language; opt->u._string = options_string[i]; ++opt; } initialized_options = true; } void config_read(Ted *ted, ConfigPart **parts, char const *filename) { FILE *fp = fopen(filename, "rb"); if (!fp) { ted_seterr(ted, "Couldn't open config file %s.", filename); return; } ConfigReader cfg_reader = { .ted = ted, .filename = filename, .line_number = 1, .error = false }; ConfigReader *cfg = &cfg_reader; ConfigPart *part = NULL; char line[4096] = {0}; while (fgets(line, sizeof line, fp)) { char *newline = strchr(line, '\n'); if (!newline && !feof(fp)) { config_err(cfg, "Line is too long."); break; } if (newline) *newline = '\0'; char *carriage_return = strchr(line, '\r'); if (carriage_return) *carriage_return = '\0'; if (line[0] == '[') { // a new part! part = arr_addp(*parts); part->index = (int)arr_len(*parts); part->file = str_dup(filename); part->line = cfg_reader.line_number + 1; parse_section_header(&cfg_reader, line, part); } else if (part) { for (int i = 0; line[i]; ++i) { arr_add(part->text, line[i]); } arr_add(part->text, '\n'); } else { const char *p = line; while (isspace(*p)) ++p; if (*p == '\0' || *p == '#') { // blank line } else { config_err(cfg, "Config has text before first section header."); } } } if (ferror(fp)) ted_seterr(ted, "Error reading %s.", filename); fclose(fp); } // IMPORTANT REQUIREMENT FOR THIS FUNCTION: // - less specific contexts compare as less // (i.e. if context_is_parent(a.context, b.context), then we return -1, and vice versa.) // if total = true, this gives a total ordering // if total = false, parts with identical contexts will compare equal. static int config_part_cmp(const ConfigPart *ap, const ConfigPart *bp, bool total) { const SettingsContext *a = &ap->context, *b = &bp->context; if (a->language == 0 && b->language != 0) return -1; if (a->language != 0 && b->language == 0) return +1; const char *a_path = a->path ? a->path : ""; const char *b_path = b->path ? b->path : ""; size_t a_path_len = strlen(a_path), b_path_len = strlen(b_path); if (a_path_len < b_path_len) return -1; if (a_path_len > b_path_len) return 1; // done with specificity, now on to identicalness if (a->language < b->language) return -1; if (a->language > b->language) return +1; int cmp = strcmp(a_path, b_path); if (cmp != 0) return cmp; if (total) { if (ap->index < bp->index) return -1; if (ap->index > bp->index) return +1; } return 0; } static int config_part_qsort_cmp(const void *av, const void *bv) { return config_part_cmp(av, bv, true); } static void config_parse_line(ConfigReader *cfg, Settings *settings, const ConfigPart *part, const char *line) { Ted *ted = cfg->ted; if (part->section == 0) { // there was an error reading this section. don't bother with anything else. return; } switch (line[0]) { case '#': // comment case '\0': // blank line return; } char *equals = strchr(line, '='); if (!equals) { config_err(cfg, "Invalid line syntax. " "Lines should either look like [section-name] or key = value"); return; } char const *key = line; *equals = '\0'; char const *value = equals + 1; while (isspace(*key)) ++key; while (isspace(*value)) ++value; if (equals != line) { for (char *p = equals - 1; p > line; --p) { // remove trailing spaces after key if (isspace(*p)) *p = '\0'; else break; } } if (key[0] == '\0') { config_err(cfg, "Empty property name. This line should look like: key = value"); return; } switch (part->section) { case SECTION_NONE: config_err(cfg, "Line outside of any section." "Try putting a section header, e.g. [keyboard] before this line?"); break; case SECTION_COLORS: { ColorSetting setting = color_setting_from_str(key); if (setting != COLOR_UNKNOWN) { u32 color = 0; if (color_from_str(value, &color)) { settings->colors[setting] = color; } else { config_err(cfg, "'%s' is not a valid color. Colors should look like #rgb, #rgba, #rrggbb, or #rrggbbaa.", value); } } else { config_err(cfg, "No such color option: %s", key); } } break; case SECTION_KEYBOARD: { // lines like Ctrl+Down = 10 :down u32 key_combo = config_parse_key_combo(cfg, key); KeyAction *action = &settings->key_actions[key_combo]; llong argument = 1; if (isdigit(*value)) { // read the argument char *endp; argument = strtoll(value, &endp, 10); value = endp; } else if (*value == '"') { // string argument int backslashes = 0; char const *p; for (p = value + 1; *p; ++p) { bool done = false; switch (*p) { case '\\': ++backslashes; break; case '"': if (backslashes % 2 == 0) done = true; break; } if (done) break; } if (!*p) { config_err(cfg, "String doesn't end."); break; } if (ted->nstrings < TED_MAX_STRINGS) { char *str = strn_dup(value + 1, (size_t)(p - (value + 1))); argument = ted->nstrings | ARG_STRING; ted->strings[ted->nstrings++] = str; } value = p + 1; } while (isspace(*value)) ++value; // skip past space following argument if (*value == ':') { // read the command Command command = command_from_str(value + 1); if (command != CMD_UNKNOWN) { action->command = command; action->argument = argument; action->line_number = cfg->line_number; } else { config_err(cfg, "Unrecognized command %s", value); } } else { config_err(cfg, "Expected ':' for key action. This line should look something like: %s = :command.", key); } } break; case SECTION_EXTENSIONS: { Language lang = language_from_str(key); if (lang == LANG_NONE) { config_err(cfg, "Invalid programming language: %s.", key); } else { char *new_str = malloc(strlen(value) + 1); if (!new_str) { config_err(cfg, "Out of memory."); } else { char *dst = new_str; // get rid of whitespace in extension list for (char const *src = value; *src; ++src) if (!isspace(*src)) *dst++ = *src; *dst = 0; if (settings->language_extensions[lang]) free(settings->language_extensions[lang]); settings->language_extensions[lang] = new_str; } } } break; case SECTION_CORE: { char const *endptr; long long const integer = strtoll(value, (char **)&endptr, 10); bool const is_integer = *endptr == '\0'; double const floating = strtod(value, (char **)&endptr); bool const is_floating = *endptr == '\0'; bool is_bool = false; bool boolean = false; #define BOOL_HELP "(should be yes/no/on/off)" if (streq(value, "yes") || streq(value, "on")) { is_bool = true; boolean = true; } else if (streq(value, "no") || streq(value, "off")) { is_bool = true; boolean = false; } // go through all options bool recognized = false; for (size_t i = 0; i < arr_count(all_options) && !recognized; ++i) { OptionAny const *any = &all_options[i]; if (any->type == 0) break; if (streq(key, any->name)) { recognized = true; if (part->context.language != 0 && !any->per_language) { config_err(cfg, "Option %s cannot be controlled for individual languages.", key); break; } switch (any->type) { case OPTION_BOOL: { OptionBool const *option = &any->u._bool; if (is_bool) option_bool_set(settings, option, boolean); else config_err(cfg, "Invalid %s: %s. This should be yes, no, on, or off.", option->name, value); } break; case OPTION_U8: { OptionU8 const *option = &any->u._u8; if (is_integer && integer >= option->min && integer <= option->max) option_u8_set(settings, option, (u8)integer); else config_err(cfg, "Invalid %s: %s. This should be an integer from %u to %u.", option->name, value, option->min, option->max); } break; case OPTION_U16: { OptionU16 const *option = &any->u._u16; if (is_integer && integer >= option->min && integer <= option->max) option_u16_set(settings, option, (u16)integer); else config_err(cfg, "Invalid %s: %s. This should be an integer from %u to %u.", option->name, value, option->min, option->max); } break; case OPTION_FLOAT: { OptionFloat const *option = &any->u._float; if (is_floating && floating >= option->min && floating <= option->max) option_float_set(settings, option, (float)floating); else config_err(cfg, "Invalid %s: %s. This should be a number from %g to %g.", option->name, value, option->min, option->max); } break; case OPTION_STRING: { OptionString const *option = &any->u._string; if (strlen(value) >= option->buf_size) { config_err(cfg, "%s is too long (length: %zu, maximum length: %zu).", key, strlen(value), option->buf_size - 1); } else { option_string_set(settings, option, value); } } break; } } } // this is probably a bad idea: //if (!recognized) // config_err(cfg, "Unrecognized option: %s", key); // because if we ever remove an option in the future // everyone will get errors } break; } } void config_parse(Ted *ted, ConfigPart **pparts) { config_init_options(); ConfigReader cfg_reader = { .ted = ted, .filename = NULL, .line_number = 1, .error = false }; ConfigReader *cfg = &cfg_reader; ConfigPart *const parts = *pparts; qsort(parts, arr_len(parts), sizeof *parts, config_part_qsort_cmp); Settings *settings = NULL; arr_foreach_ptr(parts, ConfigPart, part) { cfg->filename = part->file; cfg->line_number = part->line; if (part == parts || config_part_cmp(part, part - 1, false) != 0) { // new settings settings = arr_addp(ted->all_settings); // go backwards to find most specific parent ConfigPart *parent = part; while (1) { if (parent <= parts) { parent = NULL; break; } --parent; if (context_is_parent(&parent->context, &part->context)) { // copy parent's settings settings_copy(settings, parent->settings); break; } } context_free(&settings->context); context_copy(&settings->context, &part->context); } part->settings = settings; arr_add(part->text, '\0'); // null termination char *line = part->text; while (*line) { char *newline = strchr(line, '\n'); if (!newline) { config_err(cfg, "No newline at end of file?"); break; } if (newline) *newline = '\0'; char *carriage_return = strchr(line, '\r'); if (carriage_return) *carriage_return = '\0'; config_parse_line(cfg, settings, part, line); if (cfg->error) break; ++cfg->line_number; line = newline + 1; } } arr_foreach_ptr(ted->all_settings, Settings, s) { SettingsContext *ctx = &s->context; if (ctx->language == 0 && (!ctx->path || !*ctx->path)) { ted->default_settings = s; break; } } arr_foreach_ptr(parts, ConfigPart, part) { config_part_free(part); } arr_clear(*pparts); } void config_free(Ted *ted) { arr_foreach_ptr(ted->all_settings, Settings, s) { settings_free(s); } for (u32 i = 0; i < ted->nstrings; ++i) { free(ted->strings[i]); } arr_clear(ted->all_settings); ted->default_settings = NULL; }