summaryrefslogtreecommitdiff
path: root/config.c
diff options
context:
space:
mode:
authorpommicket <pommicket@gmail.com>2022-11-02 13:02:38 -0400
committerpommicket <pommicket@gmail.com>2022-11-02 13:02:38 -0400
commit26d34216da04a2b91e65a0eeee9200ad808d48ce (patch)
treee9781ff976f16f1f5249ca679aa2c941a35eb03c /config.c
parentb63bd066bf06317e6637aa03369a75c018c1939f (diff)
(insufficiently tested) per-path settings
also fixed memory bug in path_full, yet again
Diffstat (limited to 'config.c')
-rw-r--r--config.c830
1 files changed, 491 insertions, 339 deletions
diff --git a/config.c b/config.c
index 3ed27d8..7a47d23 100644
--- a/config.c
+++ b/config.c
@@ -7,16 +7,10 @@
// [section2]
// asdf = 123
-typedef enum {
- SECTION_NONE,
- SECTION_CORE,
- SECTION_KEYBOARD,
- SECTION_COLORS,
- SECTION_EXTENSIONS
-} Section;
-
// all worth it for the -Wformat warnings
-#define config_err(cfg, ...) do { snprintf((cfg)->ted->error, sizeof (cfg)->ted->error - 1, "%s:%u: ", (cfg)->filename, (cfg)->line_number), \
+#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)
@@ -27,6 +21,51 @@ typedef struct {
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) {
+ context_copy(&dest->context, &src->context);
+ for (u32 i = 0; i < LANG_COUNT; ++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;
@@ -172,7 +211,7 @@ typedef struct {
} OptionU16;
typedef struct {
char const *name;
- char *control;
+ const char *control;
size_t buf_size;
bool per_language;
} OptionString;
@@ -197,127 +236,173 @@ typedef struct {
} 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 + (size_t)opt->control) = 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 + (size_t)opt->control) = value;
+ *(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 + (size_t)opt->control) = value;
+ *(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 + (size_t)opt->control) = value;
+ *(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 + (size_t)opt->control;
+ char *control = (char *)settings + (opt->control - (char*)&options_zero);
str_cpy(control, opt->buf_size, value);
}
-// two passes are done
-// pass 0 reads global settings
-// pass 1 reads language-specific settings
-void config_read(Ted *ted, char const *filename, int pass) {
- ConfigReader cfg_reader = {
- .ted = ted,
- .filename = filename,
- .line_number = 1,
- .error = false
- };
- ConfigReader *cfg = &cfg_reader;
- Settings *settings = ted->settings;
-
- // core options
- // (these go at the start so they don't need to be re-computed each time)
- Settings *nullset = NULL;
- OptionBool const options_bool[] = {
- {"auto-indent", &nullset->auto_indent, true},
- {"auto-add-newline", &nullset->auto_add_newline, true},
- {"auto-reload", &nullset->auto_reload, true},
- {"auto-reload-config", &nullset->auto_reload_config, false},
- {"syntax-highlighting", &nullset->syntax_highlighting, true},
- {"line-numbers", &nullset->line_numbers, true},
- {"restore-session", &nullset->restore_session, false},
- {"regenerate-tags-if-not-found", &nullset->regenerate_tags_if_not_found, true},
- {"indent-with-spaces", &nullset->indent_with_spaces, true},
- };
- OptionU8 const options_u8[] = {
- {"tab-width", &nullset->tab_width, 1, 100, true},
- {"cursor-width", &nullset->cursor_width, 1, 100, true},
- {"undo-save-time", &nullset->undo_save_time, 1, 200, true},
- {"border-thickness", &nullset->border_thickness, 1, 30, false},
- {"padding", &nullset->padding, 0, 100, false},
- {"scrolloff", &nullset->scrolloff, 1, 100, true},
- {"tags-max-depth", &nullset->tags_max_depth, 1, 100, false},
- };
- OptionU16 const options_u16[] = {
- {"text-size", &nullset->text_size, TEXT_SIZE_MIN, TEXT_SIZE_MAX, true},
- {"max-menu-width", &nullset->max_menu_width, 10, U16_MAX, false},
- {"error-display-time", &nullset->error_display_time, 0, U16_MAX, false},
- };
- OptionFloat const options_float[] = {
- {"cursor-blink-time-on", &nullset->cursor_blink_time_on, 0, 1000, true},
- {"cursor-blink-time-off", &nullset->cursor_blink_time_off, 0, 1000, true},
- };
- OptionString const options_string[] = {
- {"build-default-command", nullset->build_default_command, sizeof nullset->build_default_command, true},
- };
+
+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")) {
+ 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 all_options[1000] = {0};
- OptionAny *all_options_end = all_options;
+ OptionAny *opt = all_options;
for (size_t i = 0; i < arr_count(options_bool); ++i) {
- OptionAny *opt = all_options_end++;
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) {
- OptionAny *opt = all_options_end++;
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) {
- OptionAny *opt = all_options_end++;
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) {
- OptionAny *opt = all_options_end++;
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) {
- OptionAny *opt = all_options_end++;
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;
}
- char line[4096] = {0};
- int line_cap = sizeof line;
+ ConfigReader cfg_reader = {
+ .ted = ted,
+ .filename = filename,
+ .line_number = 1,
+ .error = false
+ };
+ ConfigReader *cfg = &cfg_reader;
- Section section = SECTION_NONE;
- Language language = LANG_NONE;
- bool skip_section = false;
+ ConfigPart *part = NULL;
- while (fgets(line, line_cap, fp)) {
+ 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.");
@@ -327,292 +412,359 @@ void config_read(Ted *ted, char const *filename, int pass) {
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->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);
+}
- // ok, we've now read a line.
- switch (line[0]) {
- case '#': // comment
- case '\0': // blank line
- break;
- case '[': { // section header
- #define SECTION_HEADER_HELP "Section headers should look like this: [section-name]"
- char *closing = strchr(line, ']');
- if (!closing) {
- config_err(cfg, "Unmatched [. " SECTION_HEADER_HELP);
- } else if (closing[1] != '\0') {
- config_err(cfg, "Text after section. " SECTION_HEADER_HELP);
+
+// REQUIREMENTS FOR THIS FUNCTION:
+// - two configs compare equal iff their contexts are identical
+// - less specific contexts compare as less
+// (i.e. if context_is_parent(a.context, b.context), then we return -1, and vice versa.)
+static int config_part_qsort_cmp(const void *av, const void *bv) {
+ const ConfigPart *ap = av, *bp = bv;
+ 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;
+ return strcmp(a_path, b_path);
+}
+
+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 {
- *closing = '\0';
- char *section_name = line + 1;
- char *dot = strchr(section_name, '.');
-
- if (dot) {
- *dot = '\0';
- language = language_from_str(section_name);
- if (!language) {
- config_err(cfg, "Unrecognized language: %s.", section_name);
- break; // skip section name check
- }
- section_name = dot + 1;
- } else {
- language = 0;
+ 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 (streq(section_name, "keyboard")) {
- section = SECTION_KEYBOARD;
- } else if (streq(section_name, "colors")) {
- section = SECTION_COLORS;
- } else if (streq(section_name, "core")) {
- section = SECTION_CORE;
- } else if (streq(section_name, "extensions")) {
- section = SECTION_EXTENSIONS;
- } else {
- config_err(cfg, "Unrecognized section: [%s].", section_name);
+ if (part->context.language != 0 && !any->per_language) {
+ config_err(cfg, "Option %s cannot be controlled for individual languages.", key);
break;
}
- skip_section = false;
- if (language) {
- switch (section) {
- case SECTION_CORE:
- case SECTION_COLORS:
- case SECTION_KEYBOARD:
- break;
- default:
- config_err(cfg, "%s settings cannot be configured for individual languages.",
- section_name);
- break;
- }
- if (pass == 0) {
- skip_section = true;
- }
- } else {
- if (pass == 1) {
- skip_section = true;
+ 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);
}
- }
- if (pass == 1) {
- settings = &ted->settings_by_language[language];
+ } break;
}
}
- } break;
- default: {
- if (skip_section) 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_qsort_cmp(part, part - 1) != 0) {
+ // new settings
+ settings = arr_addp(ted->all_settings);
+ context_copy(&settings->context, &part->context);
- char *equals = strchr(line, '=');
- if (equals) {
- 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;
- }
+ // go backwards to find most specific parent
+ ConfigPart *parent = part;
+ while (1) {
+ if (parent <= parts) {
+ parent = NULL;
+ break;
}
- if (key[0] == '\0') {
- config_err(cfg, "Empty property name. This line should look like: key = value");
- } else {
- switch (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 (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;
- }
+ --parent;
+ if (context_is_parent(&parent->context, &part->context)) {
+ // copy parent's settings
+ settings_copy(settings, parent->settings);
+ break;
}
- } else {
- config_err(cfg, "Invalid line syntax. "
- "Lines should either look like [section-name] or key = value");
}
- } break;
}
- if (cfg->error) break;
-
- ++cfg->line_number;
+ 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->settings = s;
+ break;
+ }
+ }
- if (ferror(fp))
- ted_seterr(ted, "Error reading %s.", filename);
- fclose(fp);
+ arr_foreach_ptr(parts, ConfigPart, part) {
+ config_part_free(part);
+ }
+
+ arr_clear(*pparts);
}
void config_free(Ted *ted) {
- for (u16 i = 0; i < LANG_COUNT; ++i) {
- free(ted->settings_by_language[0].language_extensions[i]);
- for (u16 l = 0; l < LANG_COUNT; ++l) {
- // these are just aliases to settings_by_language[0].language_extensions[i]
- // (you cant change language extensions on a per language basis. that would be weird.)
- ted->settings_by_language[l].language_extensions[i] = NULL;
- }
+ 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->settings = NULL;
}