diff --git a/ui/xemu-settings.c b/ui/xemu-settings.c
new file mode 100644
index 0000000000..fee5a6bf7d
--- /dev/null
+++ b/ui/xemu-settings.c
@@ -0,0 +1,370 @@
+/*
+ * xemu Settings Management
+ *
+ * Copyright (C) 2020 Matt Borgerson
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#include
+#include
+#include
+#include
+#include
+
+#include "xemu-settings.h"
+#include "inih/ini.c" // FIXME
+
+enum config_types {
+ CONFIG_TYPE_STRING,
+ CONFIG_TYPE_INT,
+ CONFIG_TYPE_BOOL,
+ CONFIG_TYPE_ENUM,
+ CONFIG_TYPE__MAX
+};
+
+struct xemu_settings {
+ // [system]
+ char *flash_path;
+ char *bootrom_path;
+ char *hdd_path;
+ char *dvd_path;
+ char *eeprom_path;
+ int memory;
+ int short_animation; // Boolean
+
+ // [display]
+ int scale;
+
+ // [input]
+ char *controller_1_guid;
+ char *controller_2_guid;
+ char *controller_3_guid;
+ char *controller_4_guid;
+};
+
+struct enum_str_map {
+ int value;
+ const char *str;
+};
+
+static const struct enum_str_map display_scale_map[DISPLAY_SCALE__COUNT+1] = {
+ { DISPLAY_SCALE_CENTER, "center" },
+ { DISPLAY_SCALE_SCALE, "scale" },
+ { DISPLAY_SCALE_STRETCH, "stretch" },
+ { 0, NULL },
+};
+
+struct config_offset_table {
+ enum config_types type;
+ const char *section;
+ const char *name;
+ ptrdiff_t offset;
+ union {
+ const char *default_str;
+ int default_int;
+ int default_bool;
+ };
+ const struct enum_str_map *enum_map;
+} config_items[XEMU_SETTINGS__COUNT] = {
+ // Please keep organized by section
+ [XEMU_SETTINGS_SYSTEM_FLASH_PATH] = { CONFIG_TYPE_STRING, "system", "flash_path", offsetof(struct xemu_settings, flash_path), { .default_str = "" } },
+ [XEMU_SETTINGS_SYSTEM_BOOTROM_PATH] = { CONFIG_TYPE_STRING, "system", "bootrom_path", offsetof(struct xemu_settings, bootrom_path), { .default_str = "" } },
+ [XEMU_SETTINGS_SYSTEM_HDD_PATH] = { CONFIG_TYPE_STRING, "system", "hdd_path", offsetof(struct xemu_settings, hdd_path), { .default_str = "" } },
+ [XEMU_SETTINGS_SYSTEM_DVD_PATH] = { CONFIG_TYPE_STRING, "system", "dvd_path", offsetof(struct xemu_settings, dvd_path), { .default_str = "" } },
+ [XEMU_SETTINGS_SYSTEM_EEPROM_PATH] = { CONFIG_TYPE_STRING, "system", "eeprom_path", offsetof(struct xemu_settings, eeprom_path), { .default_str = "" } },
+ [XEMU_SETTINGS_SYSTEM_MEMORY] = { CONFIG_TYPE_INT, "system", "memory", offsetof(struct xemu_settings, memory), { .default_int = 64 } },
+ [XEMU_SETTINGS_SYSTEM_SHORTANIM] = { CONFIG_TYPE_BOOL, "system", "shortanim", offsetof(struct xemu_settings, short_animation), { .default_bool = 0 } },
+
+ [XEMU_SETTINGS_DISPLAY_SCALE] = { CONFIG_TYPE_ENUM, "display", "scale", offsetof(struct xemu_settings, scale), { .default_int = DISPLAY_SCALE_SCALE }, display_scale_map },
+
+ [XEMU_SETTINGS_INPUT_CONTROLLER_1_GUID] = { CONFIG_TYPE_STRING, "input", "controller_1_guid", offsetof(struct xemu_settings, controller_1_guid), { .default_str = "" } },
+ [XEMU_SETTINGS_INPUT_CONTROLLER_2_GUID] = { CONFIG_TYPE_STRING, "input", "controller_2_guid", offsetof(struct xemu_settings, controller_2_guid), { .default_str = "" } },
+ [XEMU_SETTINGS_INPUT_CONTROLLER_3_GUID] = { CONFIG_TYPE_STRING, "input", "controller_3_guid", offsetof(struct xemu_settings, controller_3_guid), { .default_str = "" } },
+ [XEMU_SETTINGS_INPUT_CONTROLLER_4_GUID] = { CONFIG_TYPE_STRING, "input", "controller_4_guid", offsetof(struct xemu_settings, controller_4_guid), { .default_str = "" } },
+};
+
+static const char *settings_path;
+static const char *filename = "xemu.ini";
+static struct xemu_settings *g_settings;
+static int settings_failed_to_load = 0;
+
+static void *xemu_settings_get_field(enum xemu_settings_keys key, enum config_types type)
+{
+ assert(key < XEMU_SETTINGS__COUNT);
+ assert(config_items[key].type == type);
+ return (void *)((char*)g_settings + config_items[key].offset);
+}
+
+int xemu_settings_set_string(enum xemu_settings_keys key, const char *str)
+{
+ char **field_str = (char **)xemu_settings_get_field(key, CONFIG_TYPE_STRING);
+ free(*field_str);
+ *field_str = strdup(str);
+ return 0;
+}
+
+int xemu_settings_get_string(enum xemu_settings_keys key, const char **str)
+{
+ *str = *(const char **)xemu_settings_get_field(key, CONFIG_TYPE_STRING);
+ return 0;
+}
+
+int xemu_settings_set_int(enum xemu_settings_keys key, int val)
+{
+ int *field_int = (int *)xemu_settings_get_field(key, CONFIG_TYPE_INT);
+ *field_int = val;
+ return 0;
+}
+
+int xemu_settings_get_int(enum xemu_settings_keys key, int *val)
+{
+ *val = *(int *)xemu_settings_get_field(key, CONFIG_TYPE_INT);
+ return 0;
+}
+
+int xemu_settings_set_bool(enum xemu_settings_keys key, int val)
+{
+ int *field_int = (int *)xemu_settings_get_field(key, CONFIG_TYPE_BOOL);
+ *field_int = val;
+ return 0;
+}
+
+int xemu_settings_get_bool(enum xemu_settings_keys key, int *val)
+{
+ *val = *(int *)xemu_settings_get_field(key, CONFIG_TYPE_BOOL);
+ return 0;
+}
+
+int xemu_settings_set_enum(enum xemu_settings_keys key, int val)
+{
+ int *field_int = (int *)xemu_settings_get_field(key, CONFIG_TYPE_ENUM);
+ *field_int = val;
+ return 0;
+}
+
+int xemu_settings_get_enum(enum xemu_settings_keys key, int *val)
+{
+ *val = *(int *)xemu_settings_get_field(key, CONFIG_TYPE_ENUM);
+ return 0;
+}
+
+const char *xemu_settings_get_path(void)
+{
+ if (settings_path != NULL) {
+ return settings_path;
+ }
+
+ // Note: Ideally SDL_GetPrefPath should be used here to determine where the
+ // settings file should be stored. However, until xemu gains a proper
+ // installer, assume it will be run in "portable mode" such that everything
+ // needed to run is all in the same directory, or specified explicitly by
+ // the user via config file.
+#if 0
+ char *base = SDL_GetPrefPath("xemu", "xemu");
+#else
+ // char *base = SDL_GetBasePath();
+ // if (base == NULL) {
+ // base = strdup("./");
+ // }
+ char *base = strdup("./");
+#endif
+ assert(base != NULL);
+ size_t base_len = strlen(base);
+ size_t filename_len = strlen(filename);
+ size_t len = base_len + filename_len + 1;
+ char *path = malloc(len);
+ memcpy(path, base, base_len);
+ free(base);
+ memcpy(path+base_len, filename, strlen(filename));
+ path[len-1] = '\0';
+ settings_path = path;
+
+ fprintf(stderr, "%s: config path: %s\n", __func__, settings_path);
+
+ return settings_path;
+}
+
+static int xemu_enum_str_to_int(const struct enum_str_map *map, const char *str, int *value)
+{
+ for (int i = 0; map[i].str != NULL; i++) {
+ if (strcmp(map[i].str, str) == 0) {
+ *value = map[i].value;
+ return 0;
+ }
+ }
+
+ return -1;
+}
+
+static int xemu_enum_int_to_str(const struct enum_str_map *map, int value, const char **str)
+{
+ for (int i = 0; map[i].str != NULL; i++) {
+ if (map[i].value == value) {
+ *str = map[i].str;
+ return 0;
+ }
+ }
+
+ return -1;
+}
+
+
+static enum xemu_settings_keys xemu_key_from_name(const char *section, const char *name)
+{
+ for (int i = 0; i < XEMU_SETTINGS__COUNT; i++) {
+ if ((strcmp(section, config_items[i].section) == 0) &&
+ (strcmp(name, config_items[i].name) == 0)) {
+ return i; // Found
+ }
+ }
+
+ return XEMU_SETTINGS_INVALID;
+}
+
+static int config_parse_callback(void *user, const char *section, const char *name, const char *value)
+{
+ // struct xemu_settings *settings = (struct xemu_settings *)user;
+ fprintf(stderr, "%s: [%s] %s = %s\n", __func__, section, name, value);
+
+ enum xemu_settings_keys key = xemu_key_from_name(section, name);
+
+ if (key == XEMU_SETTINGS_INVALID) {
+ fprintf(stderr, "Ignoring unknown key %s.%s\n", section, name);
+ return 1;
+ }
+
+ if (config_items[key].type == CONFIG_TYPE_STRING) {
+ xemu_settings_set_string(key, value);
+ } else if (config_items[key].type == CONFIG_TYPE_INT) {
+ int int_val;
+ int converted = sscanf(value, "%d", &int_val);
+ if (converted != 1) {
+ fprintf(stderr, "Error parsing %s.%s as integer. Got '%s'\n", section, name, value);
+ return 0;
+ }
+ xemu_settings_set_int(key, int_val);
+ } else if (config_items[key].type == CONFIG_TYPE_BOOL) {
+ int int_val;
+ if (strcmp(value, "true") == 0) {
+ int_val = 1;
+ } else if (strcmp(value, "false") == 0) {
+ int_val = 0;
+ } else {
+ fprintf(stderr, "Error parsing %s.%s as boolean. Got '%s'\n", section, name, value);
+ return 0;
+ }
+ xemu_settings_set_bool(key, int_val);
+ } else if (config_items[key].type == CONFIG_TYPE_ENUM) {
+ int int_val;
+ int status = xemu_enum_str_to_int(config_items[key].enum_map, value, &int_val);
+ if (status != 0) {
+ fprintf(stderr, "Error parsing %s.%s as enum. Got '%s'\n", section, name, value);
+ return 0;
+ }
+ xemu_settings_set_enum(key, int_val);
+ } else {
+ // Unimplemented
+ assert(0);
+ }
+
+ // Success
+ return 1;
+}
+
+static void xemu_settings_init_default(struct xemu_settings *settings)
+{
+ memset(settings, 0, sizeof(struct xemu_settings));
+ for (int i = 0; i < XEMU_SETTINGS__COUNT; i++) {
+ if (config_items[i].type == CONFIG_TYPE_STRING) {
+ xemu_settings_set_string(i, config_items[i].default_str);
+ } else if (config_items[i].type == CONFIG_TYPE_INT) {
+ xemu_settings_set_int(i, config_items[i].default_int);
+ } else if (config_items[i].type == CONFIG_TYPE_BOOL) {
+ xemu_settings_set_bool(i, config_items[i].default_bool);
+ } else if (config_items[i].type == CONFIG_TYPE_ENUM) {
+ xemu_settings_set_enum(i, config_items[i].default_int);
+ } else {
+ // Unimplemented
+ assert(0);
+ }
+ }
+}
+
+int xemu_settings_did_fail_to_load(void)
+{
+ return settings_failed_to_load;
+}
+
+void xemu_settings_load(void)
+{
+ // Should only call this once, at startup
+ assert(g_settings == NULL);
+
+ g_settings = malloc(sizeof(struct xemu_settings));
+ assert(g_settings != NULL);
+ xemu_settings_init_default(g_settings);
+
+ // Parse configuration file
+ int status = ini_parse(xemu_settings_get_path(),
+ config_parse_callback,
+ g_settings);
+ if (status < 0) {
+ // fprintf(stderr, "Failed to load config! Using defaults\n");
+ settings_failed_to_load = 1;
+ }
+
+ printf("Config loaded!\n");
+}
+
+int xemu_settings_save(void)
+{
+ FILE *fd = fopen(xemu_settings_get_path(), "wb");
+ assert(fd != NULL);
+
+ const char *last_section = "";
+ for (int i = 0; i < XEMU_SETTINGS__COUNT; i++) {
+ if (strcmp(last_section, config_items[i].section)) {
+ fprintf(fd, "[%s]\n", config_items[i].section);
+ last_section = config_items[i].section;
+ }
+
+ fprintf(fd, "%s = ", config_items[i].name);
+ if (config_items[i].type == CONFIG_TYPE_STRING) {
+ const char *v;
+ xemu_settings_get_string(i, &v);
+ fprintf(fd, "%s\n", v);
+ } else if (config_items[i].type == CONFIG_TYPE_INT) {
+ int v;
+ xemu_settings_get_int(i, &v);
+ fprintf(fd, "%d\n", v);
+ } else if (config_items[i].type == CONFIG_TYPE_BOOL) {
+ int v;
+ xemu_settings_get_bool(i, &v);
+ fprintf(fd, "%s\n", !!(v) ? "true" : "false");
+ } else if (config_items[i].type == CONFIG_TYPE_ENUM) {
+ int v;
+ xemu_settings_get_enum(i, &v);
+ const char *str = "";
+ xemu_enum_int_to_str(config_items[i].enum_map, v, &str);
+ fprintf(fd, "%s\n", str);
+ } else {
+ // Unimplemented
+ assert(0);
+ }
+ }
+
+ fclose(fd);
+ return 0;
+}
diff --git a/ui/xemu-settings.h b/ui/xemu-settings.h
new file mode 100644
index 0000000000..4e851fe6da
--- /dev/null
+++ b/ui/xemu-settings.h
@@ -0,0 +1,83 @@
+/*
+ * xemu Settings Management
+ *
+ * Primary storage for non-volatile user configuration. Basic key-value storage
+ * that gets saved to an INI file. All entries should be accessed through the
+ * appropriate getter/setter functions.
+ *
+ * Copyright (C) 2020 Matt Borgerson
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+#ifndef XEMU_SETTINGS_H
+#define XEMU_SETTINGS_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+enum xemu_settings_keys {
+ XEMU_SETTINGS_SYSTEM_FLASH_PATH,
+ XEMU_SETTINGS_SYSTEM_BOOTROM_PATH,
+ XEMU_SETTINGS_SYSTEM_HDD_PATH,
+ XEMU_SETTINGS_SYSTEM_EEPROM_PATH,
+ XEMU_SETTINGS_SYSTEM_DVD_PATH,
+ XEMU_SETTINGS_SYSTEM_MEMORY,
+ XEMU_SETTINGS_SYSTEM_SHORTANIM,
+ XEMU_SETTINGS_DISPLAY_SCALE,
+ XEMU_SETTINGS_INPUT_CONTROLLER_1_GUID,
+ XEMU_SETTINGS_INPUT_CONTROLLER_2_GUID,
+ XEMU_SETTINGS_INPUT_CONTROLLER_3_GUID,
+ XEMU_SETTINGS_INPUT_CONTROLLER_4_GUID,
+ XEMU_SETTINGS__COUNT,
+ XEMU_SETTINGS_INVALID = -1
+};
+
+enum DISPLAY_SCALE
+{
+ DISPLAY_SCALE_CENTER,
+ DISPLAY_SCALE_SCALE,
+ DISPLAY_SCALE_STRETCH,
+ DISPLAY_SCALE__COUNT,
+ DISPLAY_SCALE_INVALID = -1
+};
+
+// Determine whether settings were loaded or not
+int xemu_settings_did_fail_to_load(void);
+
+// Get path of the config file on disk
+const char *xemu_settings_get_path(void);
+
+// Load config file from disk, or load defaults
+void xemu_settings_load(void);
+
+// Save config file to disk
+int xemu_settings_save(void);
+
+// Config item setters/getters
+int xemu_settings_set_string(enum xemu_settings_keys key, const char *str);
+int xemu_settings_get_string(enum xemu_settings_keys key, const char **str);
+int xemu_settings_set_int(enum xemu_settings_keys key, int val);
+int xemu_settings_get_int(enum xemu_settings_keys key, int *val);
+int xemu_settings_set_bool(enum xemu_settings_keys key, int val);
+int xemu_settings_get_bool(enum xemu_settings_keys key, int *val);
+int xemu_settings_set_enum(enum xemu_settings_keys key, int val);
+int xemu_settings_get_enum(enum xemu_settings_keys key, int *val);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif