DVX_GUI/dvx/dvxPrefs.c

440 lines
10 KiB
C

// 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 <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// 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);
}