// dvxPrefs.c — INI-based preferences system (read/write) // // Custom INI parser and writer. Stores entries as a dynamic array of // section/key/value triples using stb_ds. Preserves insertion order // on save so the file remains human-readable. #include "dvxPrefs.h" #include #include #include #include // stb_ds dynamic arrays (implementation lives in libtasks.a) #include "thirdparty/stb_ds.h" // ============================================================ // Internal types // ============================================================ typedef struct { char *section; char *key; char *value; } PrefsEntryT; // Comment lines are stored to preserve them on save. A comment has // key=NULL and value=the full line text (including the ; prefix). // Section headers have key=NULL and value=NULL. static PrefsEntryT *sEntries = NULL; // stb_ds dynamic array static char *sFilePath = NULL; // path used by prefsLoad (for prefsSave) // ============================================================ // Helpers // ============================================================ static char *dupStr(const char *s) { if (!s) { return NULL; } size_t len = strlen(s); char *d = (char *)malloc(len + 1); if (d) { memcpy(d, s, len + 1); } return d; } static void freeEntry(PrefsEntryT *e) { free(e->section); free(e->key); free(e->value); e->section = NULL; e->key = NULL; e->value = NULL; } // Case-insensitive string compare static int strcmpci(const char *a, const char *b) { for (;;) { int d = tolower((unsigned char)*a) - tolower((unsigned char)*b); if (d != 0 || !*a) { return d; } a++; b++; } } // Find an entry by section+key (case-insensitive). Returns index or -1. static int32_t findEntry(const char *section, const char *key) { for (int32_t i = 0; i < arrlen(sEntries); i++) { PrefsEntryT *e = &sEntries[i]; if (e->key && e->section && strcmpci(e->section, section) == 0 && strcmpci(e->key, key) == 0) { return i; } } return -1; } // Find the index of a section header entry. Returns -1 if not found. static int32_t findSection(const char *section) { for (int32_t i = 0; i < arrlen(sEntries); i++) { PrefsEntryT *e = &sEntries[i]; if (!e->key && !e->value && e->section && strcmpci(e->section, section) == 0) { return i; } } return -1; } // Trim leading/trailing whitespace in place. Returns pointer into buf. static char *trimInPlace(char *buf) { while (*buf == ' ' || *buf == '\t') { buf++; } char *end = buf + strlen(buf) - 1; while (end >= buf && (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) { *end-- = '\0'; } return buf; } // ============================================================ // prefsFree // ============================================================ void prefsFree(void) { for (int32_t i = 0; i < arrlen(sEntries); i++) { freeEntry(&sEntries[i]); } arrfree(sEntries); sEntries = NULL; free(sFilePath); sFilePath = NULL; } // ============================================================ // prefsGetBool // ============================================================ bool prefsGetBool(const char *section, const char *key, bool defaultVal) { const char *val = prefsGetString(section, key, NULL); if (!val) { return defaultVal; } char c = (char)tolower((unsigned char)val[0]); if (c == 't' || c == 'y' || c == '1') { return true; } if (c == 'f' || c == 'n' || c == '0') { return false; } return defaultVal; } // ============================================================ // prefsGetInt // ============================================================ int32_t prefsGetInt(const char *section, const char *key, int32_t defaultVal) { const char *val = prefsGetString(section, key, NULL); if (!val) { return defaultVal; } char *end = NULL; long n = strtol(val, &end, 10); if (end == val) { return defaultVal; } return (int32_t)n; } // ============================================================ // prefsGetString // ============================================================ const char *prefsGetString(const char *section, const char *key, const char *defaultVal) { int32_t idx = findEntry(section, key); if (idx < 0) { return defaultVal; } return sEntries[idx].value; } // ============================================================ // prefsLoad // ============================================================ bool prefsLoad(const char *filename) { prefsFree(); FILE *fp = fopen(filename, "rb"); if (!fp) { return false; } sFilePath = dupStr(filename); char line[512]; char *currentSection = dupStr(""); while (fgets(line, sizeof(line), fp)) { // Strip trailing whitespace/newline char *end = line + strlen(line) - 1; while (end >= line && (*end == '\r' || *end == '\n' || *end == ' ' || *end == '\t')) { *end-- = '\0'; } char *p = line; // Skip leading whitespace while (*p == ' ' || *p == '\t') { p++; } // Blank line — store as comment to preserve formatting if (*p == '\0') { PrefsEntryT e = {0}; e.section = dupStr(currentSection); e.value = dupStr(""); arrput(sEntries, e); continue; } // Comment line if (*p == ';' || *p == '#') { PrefsEntryT e = {0}; e.section = dupStr(currentSection); e.value = dupStr(line); arrput(sEntries, e); continue; } // Section header if (*p == '[') { char *close = strchr(p, ']'); if (close) { *close = '\0'; free(currentSection); currentSection = dupStr(trimInPlace(p + 1)); PrefsEntryT e = {0}; e.section = dupStr(currentSection); arrput(sEntries, e); } continue; } // Key=value char *eq = strchr(p, '='); if (eq) { *eq = '\0'; PrefsEntryT e = {0}; e.section = dupStr(currentSection); e.key = dupStr(trimInPlace(p)); e.value = dupStr(trimInPlace(eq + 1)); arrput(sEntries, e); } } free(currentSection); fclose(fp); return true; } // ============================================================ // prefsRemove // ============================================================ void prefsRemove(const char *section, const char *key) { int32_t idx = findEntry(section, key); if (idx >= 0) { freeEntry(&sEntries[idx]); arrdel(sEntries, idx); } } // ============================================================ // prefsSave // ============================================================ bool prefsSave(void) { if (!sFilePath) { return false; } return prefsSaveAs(sFilePath); } // ============================================================ // prefsSaveAs // ============================================================ bool prefsSaveAs(const char *filename) { FILE *fp = fopen(filename, "wb"); if (!fp) { return false; } const char *lastSection = ""; for (int32_t i = 0; i < arrlen(sEntries); i++) { PrefsEntryT *e = &sEntries[i]; // Comment or blank line (key=NULL, value=text or empty) if (!e->key && e->value) { fprintf(fp, "%s\r\n", e->value); continue; } // Section header (key=NULL, value=NULL) if (!e->key && !e->value) { fprintf(fp, "[%s]\r\n", e->section); lastSection = e->section; continue; } // Key=value if (e->key && e->value) { fprintf(fp, "%s = %s\r\n", e->key, e->value); } } fclose(fp); return true; } // ============================================================ // prefsSetBool // ============================================================ void prefsSetBool(const char *section, const char *key, bool value) { prefsSetString(section, key, value ? "true" : "false"); } // ============================================================ // prefsSetInt // ============================================================ void prefsSetInt(const char *section, const char *key, int32_t value) { char buf[32]; snprintf(buf, sizeof(buf), "%ld", (long)value); prefsSetString(section, key, buf); } // ============================================================ // prefsSetString // ============================================================ void prefsSetString(const char *section, const char *key, const char *value) { int32_t idx = findEntry(section, key); if (idx >= 0) { // Update existing entry free(sEntries[idx].value); sEntries[idx].value = dupStr(value); return; } // Find or create section header int32_t secIdx = findSection(section); if (secIdx < 0) { // Add blank line before new section (unless file is empty) if (arrlen(sEntries) > 0) { PrefsEntryT blank = {0}; blank.section = dupStr(section); blank.value = dupStr(""); arrput(sEntries, blank); } // Add section header PrefsEntryT secEntry = {0}; secEntry.section = dupStr(section); arrput(sEntries, secEntry); secIdx = arrlen(sEntries) - 1; } // Find insertion point: after last entry in this section int32_t insertAt = secIdx + 1; while (insertAt < arrlen(sEntries)) { PrefsEntryT *e = &sEntries[insertAt]; // Stop if we've hit a different section header if (!e->key && !e->value && e->section && strcmpci(e->section, section) != 0) { break; } // Stop if we've hit an entry from a different section if (e->section && strcmpci(e->section, section) != 0) { break; } insertAt++; } // Insert new entry PrefsEntryT newEntry = {0}; newEntry.section = dupStr(section); newEntry.key = dupStr(key); newEntry.value = dupStr(value); arrins(sEntries, insertAt, newEntry); }