733 lines
21 KiB
C
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, ¤tSection);
|
|
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);
|
|
}
|