UI: seven themes, each dark and light, chosen in the Settings

Settings > Theme and Settings > Light or dark pick the colours of every
screen (issue #10): roro9stack (the website's, the default), Catppuccin,
Dracula, Nord, ANSI terminal, Gruvbox and Solarized.

- A palette is 14 roles (lib/ui/src/palette.h). theme::kText and the
  others are now references into the one in use, so the Apps are
  unchanged; the main loop applies it when a Setting changes.
- Every colour is one the RGB332 frame buffer holds exactly: the themes'
  own were rounded, and those that fell together or lost contrast were
  picked by hand. test_palette checks exactness and 18 contrast ratios
  for each palette.
- The Sky view's constellation colours come from the theme.
- `theme [0-6] [light|dark]` on the consoles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
This commit is contained in:
2026-10-10 00:11:52 +02:00
co-authored by Claude Opus 5.5
parent 10627e6e18
commit 37335b4829
17 changed files with 336 additions and 24 deletions
+19 -1
View File
@@ -193,6 +193,7 @@ void setup() {
Serial.begin(115200);
settings.load();
theme::apply(ui::palette(settings.getInt(Setting::Theme), settings.getBool(Setting::ThemeLight)));
if (safeMode) return setupSafeMode(crashes);
battery = new BatteryService(bus);
@@ -694,7 +695,7 @@ static const char* const kHelp =
"irc start | irc stop | irc dump | irc say <buffer> <text>\n"
"install <path.ota> Update from SD\n"
"update check | update list | update status | update install <tag> the project's releases on Gitea\n"
"sd card | sd list | cat <path> | log <text> | burst | sound on|off | short | normal\n"
"sd card | sd list | cat <path> | log <text> | burst | sound on|off | theme [0-6] [light|dark] | short | normal\n"
"Irc | Wifi | Gnss | Gemini | Lora | Storage | Notes | Shell | System | Settings open that App: a capital letter is an App, not a command\n"
"ping <host> [count] [size] | nslookup <name> [server] | port <host> <port> | traceroute <host> | cancel is it there, does its name resolve, is its port open, which way; one at a time\n"
"tls <host> [port] | ntp [server] a TLS handshake: who the certificate is for, by whom, until when, and whether this device trusts it; a time server's clock against this one\n"
@@ -1328,6 +1329,14 @@ static void runCommand(String line, bool fromSerial = false) {
settings.setBool(Setting::DnsAlways, line.endsWith("on"));
wifi->serversChanged();
}
if (line == "theme" || line.startsWith("theme ")) { // theme [0-6] [light|dark]
std::string rest = line.length() > 6 ? line.substring(6).c_str() : "";
if (!rest.empty() && isdigit(static_cast<unsigned char>(rest[0]))) settings.setInt(Setting::Theme, rest[0] - '0');
if (rest.find("light") != std::string::npos) settings.setBool(Setting::ThemeLight, true);
if (rest.find("dark") != std::string::npos) settings.setBool(Setting::ThemeLight, false);
for (int t = 0; t < ui::kThemeCount; ++t)
console.printf("theme: %d %s%s\n", t, ui::themeName(t), t == settings.getInt(Setting::Theme) ? (settings.getBool(Setting::ThemeLight) ? " (light)" : " (dark)") : "");
}
if (line == "sound off") settings.setBool(Setting::Sound, false);
if (line == "sound on") settings.setBool(Setting::Sound, true);
if (line == "short") {
@@ -1567,6 +1576,15 @@ static void loopPass() {
StatusInfo status = currentStatus();
const Toast* toast = notifier->toasts().current(now);
bool toastChanged = notifier->toasts().takeChanged();
// The theme, when the Settings (or the `theme` command) changed it: every colour is drawn again.
static int shownTheme = settings.getInt(Setting::Theme) * 2 + settings.getBool(Setting::ThemeLight);
int wantedTheme = settings.getInt(Setting::Theme) * 2 + settings.getBool(Setting::ThemeLight);
if (wantedTheme != shownTheme) {
shownTheme = wantedTheme;
theme::apply(ui::palette(wantedTheme / 2, wantedTheme % 2));
screen.invalidate();
redrawAfterUpdate = true;
}
if (apps->takeRedraw() || toastChanged || status != lastStatus || redrawAfterUpdate) {
lastStatus = status;
redrawAfterUpdate = false;