feat(settings): persist the theme through the settings binding

Keeps one writer for saved state: ui.setTheme() writes through
settings.setTheme() and then reloads the palette C cannot see.
This commit is contained in:
2026-08-04 20:44:13 -04:00
parent 9e58e72bf6
commit b3b5a9b2a3
9 changed files with 31 additions and 10 deletions
+2 -1
View File
@@ -32,7 +32,8 @@ initialize beside the Lua runtime; reserve `bt` and its full-ESP32 backend for f
**A setter that requires a follow-up call is a bug in the setter.** `settings.setTimezone()`
applies the TZ itself; `settings.setRotation()` applies the frame and re-clips. The one
unavoidable exception is the theme, because C cannot reload the Lua palette — `ui.setTheme()`
is the seam that pairs them, and apps call that, never `settings.setTheme()`.
is the seam that pairs them: it writes through `settings.setTheme()`, then rebuilds the
palette and repaints. Apps call `ui.setTheme()`, never `settings.setTheme()`.
**Persisted intent is not live state.** `settings.getRotation()` is what the user saved;
`gui.getRotation()` is the frame being drawn. They diverge on purpose while an app rotates
+1 -1
View File
@@ -61,7 +61,7 @@ it calls `sys.setAppName("settings - wifi")`, which the status bar picks up on i
| `input` | `getTouch()` -> `x,y` or nil, `getRawTouch()` -> raw ADC `x,y` or nil, `isTouched()` |
| `fs` | `readFile(path)`, `writeFile(path, data)`, `exists(path)`, `listFiles(path)`, `listDirs(path)` |
| `sys` | `getMillis()`, `delay(ms)`, `back()`, `launch(path, arg)`, `replace(path, arg)`, `getAppName()`, `setAppName(name)`, `getMemory()` -> `free,total,largest`, `isClockSynced()`, `setTickInterval(ms)` |
| `settings` | `getRotation()`, `setRotation(deg)`, `getTheme()`, `setTheme(name)`, `getTimezone()`, `setTimezone(tz)`, `setCalibration(x0,y0,x1,y1)` |
| `settings` | `getRotation()`, `setRotation(deg)`, `getTimezone()`, `setTimezone(tz)`, `getTheme()`, `setTheme(name)` (stores only; apps call `ui.setTheme`), `setCalibration(x0,y0,x1,y1)` |
| `wifi` | `scan()` -> `{ssid,rssi,secure}[]`, `connect(ssid,password)`, `getStatus()` -> `{state,ssid,ip,rssi}`, `getLocalIP()`, `isConnected()`, `disconnect()`, `forget()` |
| `log` | `debug(msg)`, `info(msg)`, `error(msg)` (serial) |
+1 -1
View File
@@ -32,7 +32,7 @@ strength a status screen wants. Its `state` vocabulary is a subset: crosspoint r
| Refresh | `gui.refresh(mode)`, `REFRESH_FULL/HALF/FAST` | none | An LCD has no waveform modes. |
| Colour | `COLOR_*` constants, 4 grey levels | `gui.color(r, g, b)` returning RGB565 | 16-bit colour has too many values to enumerate. |
| Shapes | `drawRoundedRect` + `fillRoundedRect` | one `gui.roundRect(...)` with gradient and border | Fill and border derive from a single distance field, so their edges cannot disagree. |
| Themes | none | `settings.getTheme/setTheme`, `/lib/theme.lua` | Colour panel. |
| Themes | none | `settings.getTheme/setTheme`, `ui.setTheme` | Colour panel. |
| Rotation | `gui.setOrientation("portrait")` | `settings.setRotation(degrees)` persisted, `gui.setRotation(degrees)` for one frame | This device stores rotation in settings and remaps touch to match. |
| Clock | nothing exposed; UTC offset is a C++ setting | `sys.isClockSynced`, `settings.getTimezone/setTimezone` with POSIX TZ rules | Timezone here is a stored rule, so `os.date()` returns local time with DST handled by libc. |
| Launching | launcher is C++ | `sys.launch(path)`, home is a Lua app | The launcher is just another app here, named `home`. |
+2
View File
@@ -28,6 +28,8 @@ public:
esp32lua::Status setRotation(int32_t degrees) override;
std::string timezone() const override;
esp32lua::Status setTimezone(const std::string& timezone) override;
std::string theme() const override;
esp32lua::Status setTheme(const std::string& theme) override;
private:
LuaHost& host;
+12
View File
@@ -42,6 +42,18 @@ Status Settings::setTimezone(const std::string& timezone) {
: Status::failure("cannot save settings");
}
std::string Settings::theme() const { return settings.theme.c_str(); }
// Stores the name only. ui.setTheme() owns applying it, because the palette and
// the repaint it drives exist solely in Lua.
Status Settings::setTheme(const std::string& theme) {
if (theme.empty() || theme.size() > 16)
return Status::failure("theme must be 1 to 16 characters");
settings.theme = theme.c_str();
return settings.save() ? Status::success()
: Status::failure("cannot save settings");
}
int32_t Sys::millis() const { return static_cast<int32_t>(::millis()); }
esp32lua::MemoryInfo Sys::memory() const {
+2 -2
View File
@@ -69,8 +69,8 @@ bool Settings::load() {
lua_pcall(L, 0, 1, 0) == LUA_OK && lua_istable(L, -1);
if (ok) {
rotation = fieldOr(L, "rotation", rotation);
theme = stringFieldOr(L, "theme", theme);
timezone = stringFieldOr(L, "timezone", timezone);
theme = stringFieldOr(L, "theme", theme);
lua_getfield(L, -1, "touch");
if (lua_istable(L, -1)) {
touchX0 = fieldOr(L, "x0", touchX0);
@@ -92,8 +92,8 @@ bool Settings::load() {
bool Settings::save() const {
String source = "return {\n rotation = " + String(rotation) + ",\n";
source += " theme = " + luaString(theme) + ",\n";
source += " timezone = " + luaString(timezone) + ",\n";
source += " theme = " + luaString(theme) + ",\n";
source += " touch = { x0 = " + String(touchX0) +
", y0 = " + String(touchY0) + ", x1 = " + String(touchX1) +
", y1 = " + String(touchY1) + " },\n";
+3 -3
View File
@@ -11,13 +11,13 @@ struct Settings {
int16_t rotation = 0;
String wifiSsid;
String wifiPassword;
// Name of a palette in /lib/theme.lua; the colors themselves live on the
// card, so improving a theme never has to migrate saved settings.
String theme = "light";
// POSIX TZ string, applied with setenv("TZ")/tzset(). Storing the rule rather
// than a zone name means newlib handles DST changeovers and an unlisted zone
// still works.
String timezone = "UTC0";
// Name of a palette in ui.lua; the colors themselves live in Lua, so
// improving a theme never has to migrate saved settings.
String theme = "light";
uint8_t rotationIndex() const { return (rotation / 90) & 3; }
bool setRotation(int16_t degrees);
+7 -1
View File
@@ -220,6 +220,13 @@ function device.install()
device.timezone = tz
return saved()
end,
getTheme = function()
return device.theme
end,
setTheme = function(name)
device.theme = name
return saved()
end,
}
input = {
@@ -242,7 +249,6 @@ function device.install()
fs = {
MAX_READ_BYTES = 65536,
-- ui.lua persists the theme through fs, so the files table is real storage.
readFile = function(path)
return device.files[path]
end,