DVX_GUI/src/libs/kpunch/libdvx/dvxPrefs.c
2026-10-13 13:49:12 -05:00

733 lines
21 KiB
C

// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
// dvxPrefs.c -- INI-based preferences system (read/write)
//
// Handle-based: each PrefsHandleT holds its own entry array and file
// path. Multiple INI files can be open simultaneously. The file is read
// whole, so lines of any length load without truncation, and the content
// of every line not changed through the API is preserved (every line is
// written with CR LF).
#include "dvxPrefs.h"
#include "dvxTypes.h"
#include "platform/dvxPlat.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/stat.h>
#include "dvxMem.h"
#include "thirdparty/stb_ds_wrap.h"
// ============================================================
// Internal types
// ============================================================
typedef enum {
PrefsLineOtherE, // blank, comment, or neither a section nor a key
PrefsLineSectionE, // [name]
PrefsLineKeyE // key = value
} PrefsLineE;
// One physical line. raw is the line exactly as read (without its line
// ending) and is written back verbatim; it is dropped when the key's
// value changes, and entries created through the API have none, so those
// are written in canonical "key = value" / "[section]" form. Other lines
// always keep raw. section is the section the line sits in ("" before
// the first header).
typedef struct {
PrefsLineE kind;
char *section;
char *key;
char *value;
char *raw;
} PrefsEntryT;
struct PrefsHandleT {
PrefsEntryT *entries; // stb_ds dynamic array
char *filePath; // path used by prefsSave
bool loadFailed; // file exists but could not be read: prefsSave refuses
};
// A settings file saved while DVX's folder cannot be written to
// (platformIsReadOnly): kept here until DVX exits, and read back by
// prefsLoad in place of the file on disk.
typedef struct {
char *path;
char *text;
int32_t len;
} PrefsMemFileT;
// Decimal text of an int32_t including sign and NUL
#define PREFS_INT_TEXT_MAX 16
// Line ending written by prefsSaveAs
#define PREFS_EOL "\r\n"
// [video]: the display mode DVX starts in (prefsGetVideoMode)
#define PREFS_VIDEO_SECTION "video"
#define PREFS_VIDEO_WIDTH "width"
#define PREFS_VIDEO_HEIGHT "height"
#define PREFS_VIDEO_BPP "bpp"
// Words prefsGetBool accepts (case-insensitive, whole value)
static const char *const sTrueWords[] = { "true", "yes", "1" };
// Settings files held in memory (stb_ds array)
static PrefsMemFileT *sMemFiles = NULL;
static const char *const sFalseWords[] = { "false", "no", "0" };
// ============================================================
// Shared preference vocabularies
// ============================================================
const PrefsChoiceT prefsAccelChoices[PREFS_ACCEL_CHOICE_COUNT] = {
{ "Off", "off", MOUSE_ACCEL_OFF },
{ "Low", "low", MOUSE_ACCEL_LOW },
{ "Medium", "medium", MOUSE_ACCEL_MEDIUM },
{ "High", "high", MOUSE_ACCEL_HIGH }
};
const PrefsChoiceT prefsWallpaperChoices[PREFS_WALLPAPER_CHOICE_COUNT] = {
[WallpaperStretchE] = { "Stretch", "stretch", WallpaperStretchE },
[WallpaperTileE] = { "Tile", "tile", WallpaperTileE },
[WallpaperCenterE] = { "Center", "center", WallpaperCenterE }
};
const PrefsChoiceT prefsWheelDirChoices[PREFS_WHEEL_DIR_CHOICE_COUNT] = {
{ "Normal", "normal", MOUSE_WHEEL_DIR_NORMAL },
{ "Reversed", "reversed", MOUSE_WHEEL_DIR_REVERSED }
};
// ============================================================
// Prototypes
// ============================================================
static void addEntry(PrefsHandleT *h, PrefsLineE kind, const char *section, const char *key, const char *value, const char *raw);
static void appendText(char **buf, const char *a, const char *b, const char *c);
static int32_t findEntry(PrefsHandleT *h, const char *section, const char *key);
static int32_t findSection(PrefsHandleT *h, const char *section);
static char *formatText(const PrefsHandleT *h);
static void freeEntry(PrefsEntryT *e);
static bool matchWord(const char *val, const char *const *words, int32_t count);
static char *memFileCopy(const char *path, int32_t *len);
static bool memFileStore(const char *path, const char *text, int32_t len);
static void parseLine(PrefsHandleT *h, char *line, char **currentSection);
static char *strdupOrNull(const char *s);
static char *trimInPlace(char *buf);
static bool writeText(const char *filename, const char *text, int32_t len);
// Appends one entry; every string is copied (NULL stays NULL). Out of
// memory drops the entry: a half-copied one would put NULL names in front
// of findEntry's strcasecmp.
static void addEntry(PrefsHandleT *h, PrefsLineE kind, const char *section, const char *key, const char *value, const char *raw) {
PrefsEntryT e = {0};
e.kind = kind;
e.section = strdupOrNull(section);
e.key = strdupOrNull(key);
e.value = strdupOrNull(value);
e.raw = strdupOrNull(raw);
if ((section && !e.section) || (key && !e.key) || (value && !e.value) || (raw && !e.raw)) {
freeEntry(&e);
return;
}
arrput(h->entries, e);
}
// Add a, b and c (each may be NULL) to the stb_ds char array *buf.
static void appendText(char **buf, const char *a, const char *b, const char *c) {
const char *parts[] = { a, b, c };
for (int32_t i = 0; i < (int32_t)(sizeof(parts) / sizeof(parts[0])); i++) {
for (const char *p = parts[i]; p && *p; p++) {
arrput(*buf, *p);
}
}
}
static int32_t findEntry(PrefsHandleT *h, const char *section, const char *key) {
for (int32_t i = 0; i < arrlen(h->entries); i++) {
PrefsEntryT *e = &h->entries[i];
if (e->kind == PrefsLineKeyE && strcasecmp(e->section, section) == 0 && strcasecmp(e->key, key) == 0) {
return i;
}
}
return -1;
}
static int32_t findSection(PrefsHandleT *h, const char *section) {
for (int32_t i = 0; i < arrlen(h->entries); i++) {
PrefsEntryT *e = &h->entries[i];
if (e->kind == PrefsLineSectionE && strcasecmp(e->section, section) == 0) {
return i;
}
}
return -1;
}
// The file's text: every entry, CR LF after each (stb_ds char array, no
// NUL; NULL when there are no entries).
static char *formatText(const PrefsHandleT *h) {
char *buf = NULL;
for (int32_t i = 0; i < arrlen(h->entries); i++) {
const PrefsEntryT *e = &h->entries[i];
if (e->raw) {
appendText(&buf, e->raw, PREFS_EOL, NULL);
} else if (e->kind == PrefsLineSectionE) {
appendText(&buf, "[", e->section, "]" PREFS_EOL);
} else if (e->kind == PrefsLineKeyE) {
appendText(&buf, e->key, " = ", e->value);
appendText(&buf, PREFS_EOL, NULL, NULL);
}
}
return buf;
}
static void freeEntry(PrefsEntryT *e) {
free(e->section);
free(e->key);
free(e->value);
free(e->raw);
e->section = NULL;
e->key = NULL;
e->value = NULL;
e->raw = NULL;
}
// True when val equals one of words, ignoring case.
static bool matchWord(const char *val, const char *const *words, int32_t count) {
for (int32_t i = 0; i < count; i++) {
if (strcasecmp(val, words[i]) == 0) {
return true;
}
}
return false;
}
// A copy of the text held for path (NUL-terminated, for free); NULL when
// none is held.
static char *memFileCopy(const char *path, int32_t *len) {
for (int32_t i = 0; i < arrlen(sMemFiles); i++) {
if (platformPathSame(sMemFiles[i].path, path)) {
char *copy = (char *)malloc((size_t)sMemFiles[i].len + 1);
if (copy) {
memcpy(copy, sMemFiles[i].text, (size_t)sMemFiles[i].len);
copy[sMemFiles[i].len] = '\0';
*len = sMemFiles[i].len;
}
return copy;
}
}
return NULL;
}
// Hold text as the content of path (replacing what was held).
static bool memFileStore(const char *path, const char *text, int32_t len) {
char *copy = (char *)malloc((size_t)len + 1);
if (!copy) {
return false;
}
memcpy(copy, text, (size_t)len);
copy[len] = '\0';
for (int32_t i = 0; i < arrlen(sMemFiles); i++) {
if (platformPathSame(sMemFiles[i].path, path)) {
free(sMemFiles[i].text);
sMemFiles[i].text = copy;
sMemFiles[i].len = len;
return true;
}
}
PrefsMemFileT m = { strdup(path), copy, len };
if (!m.path) {
free(copy);
return false;
}
arrput(sMemFiles, m);
return true;
}
// Classifies one physical line (modified in place) and appends it. A
// "[name]" line starts a section; a line with '=' that is not a comment
// is a key (key and value trimmed); everything else is kept verbatim.
static void parseLine(PrefsHandleT *h, char *line, char **currentSection) {
char *raw = strdup(line);
if (!raw) {
return;
}
dvxTrimRight(line);
char *p = (char *)dvxSkipWs(line);
char *close = (*p == '[') ? strchr(p, ']') : NULL;
char *eq = strchr(p, '=');
if (close) {
*close = '\0';
char *name = strdup(trimInPlace(p + 1));
if (name) {
free(*currentSection);
*currentSection = name;
addEntry(h, PrefsLineSectionE, name, NULL, NULL, raw);
}
} else if (*p == '\0' || *p == ';' || *p == '#' || *p == '[' || !eq) {
addEntry(h, PrefsLineOtherE, *currentSection, NULL, NULL, raw);
} else {
*eq = '\0';
addEntry(h, PrefsLineKeyE, *currentSection, trimInPlace(p), trimInPlace(eq + 1), raw);
}
free(raw);
}
// Index of the choice whose INI spelling is name (case-insensitive), or -1.
int32_t prefsChoiceIndex(const PrefsChoiceT *choices, int32_t count, const char *name) {
if (!name) {
return -1;
}
for (int32_t i = 0; i < count; i++) {
if (strcasecmp(name, choices[i].iniName) == 0) {
return i;
}
}
return -1;
}
// Index of the choice whose value is value, or -1.
int32_t prefsChoiceIndexOfValue(const PrefsChoiceT *choices, int32_t count, int32_t value) {
for (int32_t i = 0; i < count; i++) {
if (choices[i].value == value) {
return i;
}
}
return -1;
}
void prefsChoiceLabels(const PrefsChoiceT *choices, int32_t count, const char **labels) {
for (int32_t i = 0; i < count; i++) {
labels[i] = choices[i].label;
}
}
void prefsClose(PrefsHandleT *h) {
if (!h) {
return;
}
for (int32_t i = 0; i < arrlen(h->entries); i++) {
freeEntry(&h->entries[i]);
}
arrfree(h->entries);
free(h->filePath);
free(h);
}
PrefsHandleT *prefsCreate(void) {
PrefsHandleT *h = (PrefsHandleT *)calloc(1, sizeof(PrefsHandleT));
return h;
}
bool prefsGetBool(PrefsHandleT *h, const char *section, const char *key, bool defaultVal) {
const char *val = prefsGetString(h, section, key, NULL);
if (!val) {
return defaultVal;
}
if (matchWord(val, sTrueWords, DVX_ARRAY_LEN(sTrueWords))) {
return true;
}
if (matchWord(val, sFalseWords, DVX_ARRAY_LEN(sFalseWords))) {
return false;
}
return defaultVal;
}
int32_t prefsGetChoice(PrefsHandleT *h, const char *section, const char *key, const PrefsChoiceT *choices, int32_t count, const char *defaultName) {
int32_t idx = prefsChoiceIndex(choices, count, prefsGetString(h, section, key, NULL));
return (idx >= 0) ? idx : prefsChoiceIndex(choices, count, defaultName);
}
int32_t prefsGetInt(PrefsHandleT *h, const char *section, const char *key, int32_t defaultVal) {
const char *val = prefsGetString(h, 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;
}
const char *prefsGetString(PrefsHandleT *h, const char *section, const char *key, const char *defaultVal) {
if (!h || !section || !key) {
return defaultVal;
}
int32_t idx = findEntry(h, section, key);
if (idx < 0) {
return defaultVal;
}
return h->entries[idx].value;
}
void prefsGetVideoMode(PrefsHandleT *h, int32_t *width, int32_t *height, int32_t *bpp) {
*width = prefsGetInt(h, PREFS_VIDEO_SECTION, PREFS_VIDEO_WIDTH, DVX_DEFAULT_VIDEO_W);
*height = prefsGetInt(h, PREFS_VIDEO_SECTION, PREFS_VIDEO_HEIGHT, DVX_DEFAULT_VIDEO_H);
*bpp = prefsGetInt(h, PREFS_VIDEO_SECTION, PREFS_VIDEO_BPP, DVX_DEFAULT_VIDEO_BPP);
}
// Walks the section's keys in file order, counting only the first of any
// repeated name (the one findEntry, and so every getter, returns).
const char *prefsKeyName(PrefsHandleT *h, const char *section, int32_t index) {
if (!h || !section || index < 0) {
return NULL;
}
int32_t seen = 0;
for (int32_t i = 0; i < arrlen(h->entries); i++) {
const PrefsEntryT *e = &h->entries[i];
if (e->kind != PrefsLineKeyE || strcasecmp(e->section, section) != 0 || findEntry(h, section, e->key) != i) {
continue;
}
if (seen == index) {
return e->key;
}
seen++;
}
return NULL;
}
// Reads the whole file and splits it on LF (a CR before the LF is part
// of the line ending), so no line length limit applies.
PrefsHandleT *prefsLoad(const char *filename) {
PrefsHandleT *h = filename ? prefsCreate() : NULL;
if (!h) {
return NULL;
}
h->filePath = strdup(filename);
int32_t len = 0;
char *text = memFileCopy(filename, &len);
struct stat st;
char bakPath[DVX_MAX_PATH];
if (!text) {
text = platformReadFile(filename, &len);
}
// Missing: a crash inside platformReplaceFile may have left the only
// copy under the backup name. A save then writes it back to filename.
if (!text && stat(filename, &st) != 0 && platformPathSibling(bakPath, sizeof(bakPath), filename, PLATFORM_BAK_EXT)) {
text = platformReadFile(bakPath, &len);
}
if (!text) {
// Present but unreadable: never let a save replace it with the
// defaults this handle would hold.
h->loadFailed = (stat(filename, &st) == 0);
return h;
}
char *currentSection = strdup("");
char *line = text;
char *end = text + len;
while (currentSection && line < end) {
char *eol = (char *)memchr(line, '\n', (size_t)(end - line));
char *next = eol ? eol + 1 : end;
char *stop = eol ? eol : end;
if (stop > line && stop[-1] == '\r') {
stop--;
}
*stop = '\0';
parseLine(h, line, &currentSection);
line = next;
}
free(currentSection);
free(text);
return h;
}
void prefsRemove(PrefsHandleT *h, const char *section, const char *key) {
if (!h || !section || !key) {
return;
}
int32_t idx = findEntry(h, section, key);
if (idx >= 0) {
freeEntry(&h->entries[idx]);
arrdel(h->entries, idx);
}
}
bool prefsSave(PrefsHandleT *h) {
if (!h || !h->filePath || h->loadFailed) {
return false;
}
return prefsSaveAs(h, h->filePath);
}
// Writes the file (see writeText). When DVX's folder cannot be written
// to (read-only media) and the write fails, the text is kept in memory
// instead and prefsLoad reads it back until DVX exits.
bool prefsSaveAs(PrefsHandleT *h, const char *filename) {
if (!h || !filename) {
return false;
}
char *text = formatText(h);
int32_t len = (int32_t)arrlen(text);
bool ok = writeText(filename, text ? text : "", len);
if (!ok && platformIsReadOnly()) {
ok = memFileStore(filename, text ? text : "", len);
}
arrfree(text);
return ok;
}
void prefsSetBool(PrefsHandleT *h, const char *section, const char *key, bool value) {
prefsSetString(h, section, key, value ? "true" : "false");
}
void prefsSetInt(PrefsHandleT *h, const char *section, const char *key, int32_t value) {
char buf[PREFS_INT_TEXT_MAX];
snprintf(buf, sizeof(buf), "%ld", (long)value);
prefsSetString(h, section, key, buf);
}
void prefsSetString(PrefsHandleT *h, const char *section, const char *key, const char *value) {
if (!h || !section || !key) {
return;
}
// Normalize NULL to empty so every stored value is a valid string:
// otherwise prefsGetString returns NULL instead of the default and
// prefsSaveAs silently drops the key.
if (!value) {
value = "";
}
int32_t idx = findEntry(h, section, key);
if (idx >= 0) {
PrefsEntryT *e = &h->entries[idx];
// An unchanged value keeps the line's original spelling
if (strcmp(e->value, value) == 0) {
return;
}
char *copy = strdup(value);
if (copy) {
free(e->value);
free(e->raw);
e->value = copy;
e->raw = NULL;
}
return;
}
int32_t secIdx = findSection(h, section);
if (secIdx < 0) {
if (arrlen(h->entries) > 0) {
addEntry(h, PrefsLineOtherE, section, NULL, NULL, "");
}
addEntry(h, PrefsLineSectionE, section, NULL, NULL, NULL);
secIdx = findSection(h, section);
if (secIdx < 0) {
return;
}
}
// Insert after the section's last key. Blank lines and comments
// after it are tagged with this section but belong visually to the
// next one (a separator, "; about [b]"), so the new key goes above
// them.
int32_t insertAt = secIdx + 1;
for (int32_t i = secIdx + 1; i < arrlen(h->entries); i++) {
PrefsEntryT *e = &h->entries[i];
if (e->kind == PrefsLineSectionE) {
break;
}
if (e->kind == PrefsLineKeyE) {
insertAt = i + 1;
}
}
PrefsEntryT newEntry = {0};
newEntry.kind = PrefsLineKeyE;
newEntry.section = strdup(section);
newEntry.key = strdup(key);
newEntry.value = strdup(value);
if (!newEntry.section || !newEntry.key || !newEntry.value) {
freeEntry(&newEntry);
return;
}
arrins(h->entries, insertAt, newEntry);
}
void prefsSetVideoMode(PrefsHandleT *h, int32_t width, int32_t height, int32_t bpp) {
prefsSetInt(h, PREFS_VIDEO_SECTION, PREFS_VIDEO_WIDTH, width);
prefsSetInt(h, PREFS_VIDEO_SECTION, PREFS_VIDEO_HEIGHT, height);
prefsSetInt(h, PREFS_VIDEO_SECTION, PREFS_VIDEO_BPP, bpp);
}
static char *strdupOrNull(const char *s) {
return s ? strdup(s) : NULL;
}
static char *trimInPlace(char *buf) {
char *p = (char *)dvxSkipWs(buf);
dvxTrimRight(p);
return p;
}
// Writes text to the <base>.$<ext> sibling (PLATFORM_TMP_EXT; the
// extension is replaced, so 8.3 names stay valid) and swaps it in only
// when every write succeeded, so a full disk or I/O error leaves the
// previous file intact and reports failure.
static bool writeText(const char *filename, const char *text, int32_t len) {
char tmpPath[DVX_MAX_PATH];
char bakPath[DVX_MAX_PATH];
if (!platformPathSibling(tmpPath, sizeof(tmpPath), filename, PLATFORM_TMP_EXT) || !platformPathSibling(bakPath, sizeof(bakPath), filename, PLATFORM_BAK_EXT)) {
return false;
}
FILE *fp = fopen(tmpPath, "wb");
if (!fp) {
return false;
}
bool ok = (int32_t)fwrite(text, 1, (size_t)len, fp) == len;
// fclose flushes: a late write error surfaces here
ok = (fclose(fp) == 0) && ok;
if (!ok) {
remove(tmpPath);
return false;
}
return platformReplaceFile(tmpPath, filename, bakPath);
}