feat(lua): add app navigation history

Make sys.launch push the current path and argument, sys.replace switch without pushing, and sys.back restore the previous route. The status-bar chevron uses the same back action, failed child apps return to their caller, and history is capped at eight routes. Remove the redundant Settings back card.
This commit is contained in:
2026-08-02 13:30:12 -04:00
parent 01dfc4b458
commit 0ca1fd9842
11 changed files with 147 additions and 75 deletions
+60 -23
View File
@@ -29,12 +29,38 @@ SPIClass& sdSpi = SPI;
XPT2046_Touchscreen touch(TOUCH_CS);
LuaApp app(tft, touch);
struct Route {
String path;
String arg;
bool hasArg = false;
};
static constexpr size_t MAX_HISTORY = 8;
bool halted = false;
String nextApp;
// Carried beside the path, because sys.launch()'s argument has to outlive the state that
// passed it: the sender is torn down before the receiver exists.
String nextArg;
bool nextArgSet = false;
Route currentRoute;
Route pendingRoute;
Route history[MAX_HISTORY];
size_t historyDepth = 0;
LuaApp::ExitAction pendingAction = LuaApp::ExitAction::None;
Route homeRoute() {
Route route;
route.path = HOME;
return route;
}
void pushHistory(const Route& route) {
if (historyDepth == MAX_HISTORY) {
// Home remains the eventual floor; discard the oldest intermediate route.
for (size_t i = 1; i < MAX_HISTORY - 1; ++i) history[i] = history[i + 1];
historyDepth--;
}
history[historyDepth++] = route;
}
Route previousRoute() {
return historyDepth > 0 ? history[--historyDepth] : homeRoute();
}
// Last resort only: the UI lives in Lua, so this exists purely to explain why
// nothing else could run.
@@ -50,13 +76,15 @@ void fallbackScreen(const char* message) {
halted = true;
}
void startApp(const String& path, const char* arg) {
void startApp(const Route& route) {
// Full panel until the app's own status bar module reports its height in load().
tft.setRotation(settings.rotationIndex()); // apps may have rotated the frame
tft.resetViewport(); // load() re-clips once the new app's bar reports its height
Serial.printf("launching %s free=%u largest=%u\n", path.c_str(), ESP.getFreeHeap(), ESP.getMaxAllocHeap());
if (app.load(path.c_str(), path == HOME, arg)) return;
if (path == HOME) fallbackScreen("home failed to start");
Serial.printf("launching %s free=%u largest=%u\n", route.path.c_str(), ESP.getFreeHeap(),
ESP.getMaxAllocHeap());
const char* arg = route.hasArg ? route.arg.c_str() : nullptr;
if (app.load(route.path.c_str(), historyDepth > 0, arg)) return;
if (route.path == HOME && historyDepth == 0) fallbackScreen("home failed to start");
}
void setup() {
@@ -74,7 +102,8 @@ void setup() {
}
settings.load(); // absent file keeps the built-in defaults
net::begin();
startApp(HOME, nullptr);
currentRoute = homeRoute();
startApp(currentRoute);
}
void loop() {
@@ -88,22 +117,30 @@ void loop() {
if (app.running()) {
app.loop();
if (!app.running()) {
nextApp = app.takePendingLaunch();
nextArgSet = app.hasPendingArg();
nextArg = app.takePendingArg();
pendingAction = app.takeExitAction();
pendingRoute.path = app.takePendingLaunch();
pendingRoute.hasArg = app.hasPendingArg();
pendingRoute.arg = app.takePendingArg();
}
} else {
// An app that died left its message on screen, and home is about to paint over it.
// Serial alone is no help to anyone holding the device.
if (app.takeFailure()) delay(ERROR_HOLD_MS);
// A failed child returns to its caller after leaving the message readable. Home has
// no caller, so its failure still reaches the last-resort screen in startApp().
if (app.takeFailure()) {
delay(ERROR_HOLD_MS);
if (pendingAction == LuaApp::ExitAction::None) pendingAction = LuaApp::ExitAction::Back;
}
String path = nextApp.length() > 0 ? nextApp : String(HOME);
bool hasArg = nextApp.length() > 0 && nextArgSet;
String arg = nextArg;
nextApp = "";
nextArg = "";
nextArgSet = false;
startApp(path, hasArg ? arg.c_str() : nullptr);
if (pendingAction == LuaApp::ExitAction::Launch) {
pushHistory(currentRoute);
currentRoute = pendingRoute;
} else if (pendingAction == LuaApp::ExitAction::Replace) {
currentRoute = pendingRoute;
} else {
currentRoute = previousRoute();
}
pendingAction = LuaApp::ExitAction::None;
pendingRoute = {};
startApp(currentRoute);
}
// Hands the core to the idle task instead of spinning between polls: touch reads are