feat(runtime): hand the whole app contract to /.lua/main.lua

The firmware knew four paths and called four globals, so the card could
not change its own layout or put anything around an app. It now loads one
file, and the table that file returns owns the rest: start() mounts the
route, home and data name the tree, and every callback is a field on it
rather than a global the app and its chrome would have to share.
This commit is contained in:
2026-08-04 20:51:25 -04:00
parent e85adfa757
commit e3153f57c5
5 changed files with 192 additions and 108 deletions
+72 -42
View File
@@ -178,35 +178,6 @@ int main() {
"node.draw(root)\n"
"assert(painted == 320)");
// Callbacks: only init failing stops an app, and a release fires the tap
// alias after the up.
run(state,
"events = {}\n"
"local function note(name) return function(a) events[#events + 1] = name "
".. ':' .. tostring(a) end end\n"
"function init(arg) events[#events + 1] = 'init:' .. tostring(arg) end\n"
"function draw(delta) events[#events + 1] = 'draw:' .. delta end\n"
"on_touch_down = note('down')\n"
"on_touch_up = note('up')\n"
"on_touch = note('tap')\n"
"on_button_up = note('bup')\n"
"on_button = note('btap')");
bench.gui.commits = 0;
assert(runtime.callInit("book.epub"));
runtime.callDraw(33);
runtime.callTouch(esp32lua::TouchPhase::Down, 5, 6);
runtime.callTouch(esp32lua::TouchPhase::Move, 5,
7); // the app defines no on_touch_move
runtime.callTouch(esp32lua::TouchPhase::Up, 5, 8);
runtime.callButton("confirm", false);
run(state,
"assert(table.concat(events, ' ') == "
"'init:book.epub draw:33 down:5 up:5 tap:5 bup:confirm btap:confirm')");
// One commit per visit to the app, so six calls and not seven: the release
// and its tap alias are one visible change, and the move nobody handled still
// ends a batch.
assert(bench.gui.commits == 6);
// A timer firing inside draw is still one batch.
run(state, "function draw() timer.after(1, function() end) end\n"
"nested = timer.after(1, function() draw() end)");
@@ -214,12 +185,57 @@ int main() {
runtime.callTimer(bench.timer.scheduled.back());
assert(bench.gui.commits == 1);
run(state, "function init() error('boom') end");
assert(!runtime.callInit(""));
assert(bench.log.message.find("init: ") == 0);
run(state, "function draw() error('kaboom') end");
runtime.callDraw(1); // a failed frame logs and the app keeps running
assert(bench.log.message.find("draw: ") == 0);
// Callbacks land on the table main.lua returns, only start() failing stops an
// app, and a release fires the tap alias after the up.
{
fake::Bench chrome;
chrome.fs.files["/.lua/main.lua"] =
"events = {}\n"
"local function note(name) return function(a) events[#events + 1] = "
"name .. ':' .. tostring(a) end end\n"
"return {\n"
" start = function(route, arg) events[#events + 1] = 'start:' .. "
"route .. ':' .. arg end,\n"
" draw = function(delta) if delta < 0 then error('kaboom') end\n"
" events[#events + 1] = 'draw:' .. delta end,\n"
" on_touch_down = note('down'),\n"
" on_touch_up = note('up'),\n"
" on_touch = note('tap'),\n"
" on_button_up = note('bup'),\n"
" on_button = note('btap'),\n"
"}\n";
esp32lua::Runtime hosted(chrome.providers());
assert(hosted.startApp("Reader", "book.epub"));
chrome.gui.commits = 0;
hosted.callDraw(33);
hosted.callTouch(esp32lua::TouchPhase::Down, 5, 6);
hosted.callTouch(esp32lua::TouchPhase::Move, 5,
7); // main.lua defines no on_touch_move
hosted.callTouch(esp32lua::TouchPhase::Up, 5, 8);
hosted.callButton("confirm", false);
run(hosted.state(),
"assert(table.concat(events, ' ') == "
"'start:Reader:book.epub draw:33 down:5 up:5 tap:5 bup:confirm "
"btap:confirm')");
// One commit per visit, so five calls and not six: the release and its tap
// alias are one visible change, and the move nobody handled still ends a
// batch.
assert(chrome.gui.commits == 5);
hosted.callDraw(-1); // a failed frame logs and the app keeps running
assert(chrome.log.message.find("draw: ") == 0);
assert(hosted.hasApp());
chrome.fs.files["/.lua/main.lua"] =
"return { start = function() error('boom') end }";
assert(!hosted.startApp("Reader"));
assert(chrome.log.message.find("start: ") == 0);
assert(!hosted.hasApp());
chrome.fs.files["/.lua/main.lua"] = "return 7";
assert(!hosted.startApp("Reader"));
assert(chrome.log.message == "main.lua returned no table");
}
// A feature callback without its provider is a wiring bug, not a silent
// no-op.
@@ -235,22 +251,36 @@ int main() {
"assert(input.getTouch == nil and input.isPressed ~= nil)");
}
// App loading: a fresh state per app, require reaching the app directory and
// /.lua/lib, and navigation applied between batches rather than inside a
// App loading: a fresh state per app, main.lua deciding where apps and
// modules live, and navigation applied between batches rather than inside a
// callback.
{
fake::Bench host;
// The tree is main.lua's, so the test states it the way a card would.
host.fs.files["/.lua/main.lua"] =
"package.path = '/.lua/lib/?.lua'\n"
"return {\n"
" home = 'Home',\n"
" data = '/.lua/data/?',\n"
" start = function(route, arg)\n"
" local dir = '/.lua/apps/' .. route\n"
" package.path = dir .. '/?.lua;/.lua/lib/?.lua'\n"
" app = assert(loadfile(dir .. '/main.lua'))()\n"
" app.init(arg)\n"
" end,\n"
"}\n";
host.fs.files["/.lua/lib/greet.lua"] =
"return {hello = function() return 'hi' end}";
host.fs.files["/.lua/apps/Home/main.lua"] =
"local greet = require('greet')\n"
"function init(arg) started = greet.hello() .. ':' .. tostring(arg) "
"end";
"return {init = function(arg) started = greet.hello() .. ':' .. "
"tostring(arg) end}";
host.fs.files["/.lua/apps/Reader/main.lua"] =
"local page = require('page')\n"
"function init(arg) started = page.name .. ':' .. arg end";
"return {init = function(arg) started = page.name .. ':' .. arg end}";
host.fs.files["/.lua/apps/Reader/page.lua"] = "return {name = 'page'}";
host.fs.files["/.lua/apps/Reader/Notes/main.lua"] = "function init() end";
host.fs.files["/.lua/apps/Reader/Notes/main.lua"] =
"return {init = function() end}";
esp32lua::Runtime app(host.providers());
assert(app.startApp("Home"));
@@ -289,7 +319,7 @@ int main() {
assert(!app.startApp("Absent"));
assert(!app.hasApp() && app.state() == nullptr);
host.fs.files["/.lua/apps/Broken/main.lua"] =
"function init() error('nope') end";
"return {init = function() error('nope') end}";
assert(!app.startApp("Broken"));
assert(!app.hasApp());
assert(!app.startApp("../secrets"));