Compare commits

..

8 Commits

Author SHA1 Message Date
evan 75b3a2c490 refactor(api)!: one namespace per feature, screen split out
Namespaces were shared across features: `settings` was written by core, the
panel and touch, and `input` by touch and buttons. That made "does this
firmware implement the whole feature?" a question no pointer could answer.

Each namespace now belongs to exactly one feature or to core, so a feature is
a provider pointer and the compiler validates completeness:

  gui, node       -> screen, tree, under the screen feature
  settings        -> screen (rotation, theme), sys (timezone),
                     touch (calibration)
  input           -> touch, buttons

Runtime::open() no longer requires a GuiProvider; a firmware without one runs
with no screen/tree globals and reports sys.hasFeature("screen") false.
Rotation is one value again: GuiProvider::setRotation applies and persists, so
an app rotating the panel transiently puts the old value back itself.
2026-08-05 10:26:38 -04:00
evan 445a9b2b8f fix(ui): collect after a rebuild so the heap is in a known state
A build allocates a spec table per node and drops them all at once, so an
app that scans WiFi right after a screen change met whatever the incremental
GC had got around to. Costs a few ms on a screen change; recovers ~24 KB.
2026-08-04 21:30:25 -04:00
evan 4b89b27947 feat(ui): size a grid to the app's box, not the panel
Chrome takes the top of the panel before an app builds anything, and layout
has not run yet when it does, so ui.frame() reports what the mount left it.
2026-08-04 21:15:21 -04:00
evan 25b576a3d9 refactor(gui): drop setFullscreen now that chrome is a Lua node
Fullscreen was the firmware surrendering a strip it clipped apps out of.
The strip is a sibling node now, so an app that wants the panel is chrome
choosing not to build itself.
2026-08-04 20:58:56 -04:00
evan 2fc3abc487 feat(sys): expose canGoBack for the chrome that offers the control 2026-08-04 20:55:16 -04:00
evan 3f26c4ff10 feat(ui): one mounted tree instead of a screen object per build
Chrome and the app now share a tree, so a screen is no longer something an
app constructs and holds: ui.mount() takes the function that builds the
whole thing and ui.rebuild() runs it again. Building a node after layout
is refused rather than silently resetting the arena under the panel.
2026-08-04 20:53:15 -04:00
evan e3153f57c5 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.
2026-08-04 20:51:25 -04:00
evan e85adfa757 feat(settings): persist the theme through the settings binding
The palette lives in Lua, so C++ stores the name only and ui.setTheme()
writes through here before rebuilding and repainting.
2026-08-04 20:44:08 -04:00
28 changed files with 897 additions and 794 deletions
+5 -1
View File
@@ -4,7 +4,11 @@ This is a clean contract for repositories under the same owner's control. Choose
API without preserving old names, signatures, or behavior; consumers migrate to the contract.
Every firmware implements all declarations under `lua/api/core/`. Optional hardware contracts
live under `lua/api/features/`; `sys.hasFeature(name)` guarantees the complete matching contract.
live under `lua/api/features/`, as one file or one directory per feature; `sys.hasFeature(name)`
guarantees the complete matching contract. Every namespace belongs to exactly one feature or to
core, so a feature is a provider pointer rather than a claim to validate: a panel is the `screen`
feature (`screen` and `tree`, including the saved rotation and theme), registered only when the
firmware supplies a `GuiProvider`, and calibration is `touch.setCalibration()`.
Lua-language sources stay under `lua/`; C/C++ and the vendored interpreter stay under `native/`.
Apps are fully trusted; keep permissions and sandboxing out of scope.
Firmware commits dirty display content and owns panel refresh policy. Binding annotations under
+2 -2
View File
@@ -4,7 +4,7 @@ CC ?= cc
CXX ?= c++
AR ?= ar
CONTRACTS := $(sort $(wildcard lua/api/core/*.lua lua/api/features/*.lua))
CONTRACTS := $(sort $(wildcard lua/api/core/*.lua lua/api/features/*.lua lua/api/features/*/*.lua))
LUA_C := $(sort $(wildcard native/src/vendor/lua/*.c))
LUA_OBJECTS := $(patsubst native/src/vendor/lua/%.c,_build/lua/%.o,$(LUA_C))
LUA_LIBRARY := _build/liblua.a
@@ -12,7 +12,7 @@ LUA_SMOKE := _build/lua-smoke
RUNTIME_TEST := _build/runtime-test
LUAC32 := _build/luac32
RUNTIME_CPP := $(sort $(wildcard native/src/runtime/*.cpp native/src/bindings/core/*.cpp \
native/src/bindings/features/*.cpp) native/src/embedded_modules.cpp)
native/src/bindings/features/*.cpp native/src/bindings/features/*/*.cpp) native/src/embedded_modules.cpp)
.PHONY: api test embed compiledb format
+11 -7
View File
@@ -26,10 +26,14 @@ native/
```
Every firmware implements all files under `lua/api/core/`. `sys.hasFeature(name)` declares
optional features; claiming one guarantees every API and behavior in its matching file. Features
compose, so a device may expose both `touch` and `buttons`. Display technology is not a feature:
an e-ink `GuiProvider` flattens a gradient the way `gui.color()` quantizes to grayscale, and the
firmware owns publication and waveform policy on every panel.
optional features; claiming one guarantees every API and behavior in its matching file or
directory. Features compose, so a device may expose both `touch` and `buttons`. A panel is the
`screen` feature -- the `screen` and `tree` namespaces, including the saved rotation and theme --
because a headless firmware supplies no `GuiProvider`. Every namespace belongs to exactly one
feature or to core, which is why touch calibration is `touch.setCalibration()` rather than a
shared settings namespace three features write to. Display technology is still not a feature:
an e-ink `GuiProvider` flattens a gradient the way `screen.color()` quantizes to grayscale, and
the firmware owns publication and waveform policy on every panel.
The contract is the app-facing Lua API, not the provider C++ interface. Shared binding
registrations carry LuaLS annotations; `tools/gen_api.py` mirrors them into `lua/api/`. Generated
@@ -47,7 +51,7 @@ provider is how `sys.hasFeature()` answers false, and its namespace additions ar
registered.
The declarations are a clean target, not the intersection of today's APIs. Existing apps and
firmwares migrate to it without compatibility aliases. Safe filesystem mutation, `node`, app
firmwares migrate to it without compatibility aliases. Safe filesystem mutation, app
navigation, module loading, and `ble` are core even where a firmware does not implement them yet.
Every app may use `require`; modules resolve from its app directory and `/.lua/lib`. This repository
owns portable shared modules such as `ui.lua`; firmware-specific modules stay with their firmware.
@@ -84,7 +88,7 @@ its provider is a wiring bug and says so.
The firmware commits dirty display content after callback batches and owns e-ink waveform
policy; apps do not refresh the panel manually. Apps are fully trusted with the complete core API.
Theme persistence and application belong to shared `ui.lua`, not the firmware `settings` binding.
Theme application belongs to shared `ui.lua`; `screen.setTheme()` only stores the name.
The native library vendors Lua 5.4.8 from GitHub tag `v5.4.8` and compiles it with `LUA_32BITS`.
Command-line and upstream test entry points are excluded; `luaconf.h` carries one documented guard
@@ -92,7 +96,7 @@ that lets the build flag select 32-bit number mode.
## Shared UI
Portable apps normally use the declarative `ui.lua` toolkit; `node` remains the low-level escape
Portable apps normally use the declarative `ui.lua` toolkit; `tree` remains the low-level escape
hatch. The baseline constructors are `screen`, `box`, `spacer`, `text`, `label`, `button`,
`custom`, and `confirm`. A screen accepts both touch and physical-button input.
-151
View File
@@ -1,151 +0,0 @@
---@meta
-- Generated from native/src/bindings/core/gui.cpp. Do not edit.
---@alias GuiColor integer
---@alias GuiFont integer
---@alias GuiTextStyle integer
---@class GuiLib
---@field FONT_SMALL GuiFont Small auxiliary text.
---@field FONT_UI GuiFont Normal controls and labels.
---@field FONT_BODY GuiFont Normal reading text.
---@field FONT_LARGE GuiFont Headings and prominent values.
---@field STYLE_NORMAL GuiTextStyle
---@field STYLE_BOLD GuiTextStyle
gui = {}
gui.FONT_SMALL = 0
gui.FONT_UI = 0
gui.FONT_BODY = 0
gui.FONT_LARGE = 0
gui.STYLE_NORMAL = 0
gui.STYLE_BOLD = 0
---Returns the live frame width.
---@return integer
function gui.getWidth() end
---Returns the live frame height.
---@return integer
function gui.getHeight() end
---Rotates the live frame without changing the saved preference.
---@param degrees integer 0, 90, 180, or 270 clockwise.
function gui.setRotation(degrees) end
---Returns the rotation of the live frame.
---@return integer Degrees clockwise for the live frame.
function gui.getRotation() end
---Returns an opaque native color. E-ink implementations quantize RGB to available grayscale.
---@param r integer 0 through 255.
---@param g integer 0 through 255.
---@param b integer 0 through 255.
---@return GuiColor
function gui.color(r, g, b) end
---Clears the frame.
---@param color? GuiColor Defaults to white.
function gui.clear(color) end
---Fills a rectangle.
---@param x integer
---@param y integer
---@param w integer
---@param h integer
---@param color GuiColor
function gui.fillRect(x, y, w, h, color) end
---Outlines a rectangle.
---@param x integer
---@param y integer
---@param w integer
---@param h integer
---@param color GuiColor
function gui.drawRect(x, y, w, h, color) end
---Draws a line.
---@param x1 integer
---@param y1 integer
---@param x2 integer
---@param y2 integer
---@param color GuiColor
---@param width? integer Defaults to one pixel.
function gui.drawLine(x1, y1, x2, y2, color, width) end
---Draws a single pixel.
---@param x integer
---@param y integer
---@param color GuiColor
function gui.drawPixel(x, y, color) end
---Outlines a circle.
---@param x integer Center.
---@param y integer Center.
---@param radius integer
---@param color GuiColor
---@param width? integer Defaults to one pixel.
function gui.drawCircle(x, y, radius, color, width) end
---Fills a circle.
---@param x integer Center.
---@param y integer Center.
---@param radius integer
---@param color GuiColor
---@param background? GuiColor Surface behind an anti-aliased edge.
function gui.fillCircle(x, y, radius, color, background) end
---Draws an anti-aliased rounded fill, optional gradient, and optional border in one pass.
---@param x integer
---@param y integer
---@param w integer
---@param h integer
---@param radius integer
---@param background GuiColor Surface behind the anti-aliased edge.
---@param top? GuiColor Fill, or gradient top; omitted for no fill.
---@param bottom? GuiColor Gradient bottom; defaults to top. Panels without a gradient use top.
---@param border? GuiColor Omitted for no border.
function gui.roundRect(x, y, w, h, radius, background, top, bottom, border) end
---Temporarily gives the app the full panel, including firmware chrome.
---@param on boolean
function gui.setFullscreen(on) end
---Fills a polygon.
---@param xs integer[]
---@param ys integer[]
---@param color GuiColor
function gui.fillPolygon(xs, ys, color) end
---Draws a bitmap.
---@param path string Absolute BMP path.
---@param x? integer Left edge; defaults to centered.
---@param y? integer Top edge; defaults to centered.
---@param maxWidth? integer Defaults to panel width.
---@param maxHeight? integer Defaults to panel height.
---@return true? ok
---@return string? error
function gui.drawBmp(path, x, y, maxWidth, maxHeight) end
---Measures a text run.
---@param font GuiFont Use a named gui.FONT_* role.
---@param text string
---@param style? GuiTextStyle Defaults to gui.STYLE_NORMAL.
---@return integer
function gui.getTextWidth(font, text, style) end
---Returns the line height of a font role.
---@param font GuiFont Use a named gui.FONT_* role.
---@param style? GuiTextStyle Defaults to gui.STYLE_NORMAL.
---@return integer
function gui.getFontHeight(font, style) end
---Draws a text run with its top-left corner at x, y.
---@param font GuiFont Use a named gui.FONT_* role.
---@param x integer Left edge.
---@param y integer Top edge.
---@param text string
---@param color? GuiColor Defaults to black.
---@param style? GuiTextStyle Defaults to gui.STYLE_NORMAL.
---@param background? GuiColor Omitted for transparent text.
function gui.drawText(font, x, y, text, color, style, background) end
+10 -9
View File
@@ -2,21 +2,22 @@
-- Generated from native/src/runtime/runtime.cpp. Do not edit.
-- Runtime layout:
-- /.lua/apps/<AppId>/main.lua application entry point
-- /.lua/apps/<AppId>/<Subapp>/main.lua nested route, omitted from the launcher
-- /.lua/data/<AppId>/ persistent app data, preserved across updates
-- /.lua/lib/<module>.lua shared require() modules
-- require() also searches the running application's directory
-- The firmware loads /.lua/main.lua into every fresh state and calls these on the
-- table it returns. Where apps live, what surrounds them and which of these an app
-- itself sees are all main.lua's to decide.
--
-- Fields the firmware reads: home, the route sys.back() lands on once history is
-- empty, and data, the sys.getAppDataPath() template whose ? is the app id.
--
-- The firmware does not clear the frame before calling draw(), and commits changed
-- display content after each callback batch using the panel's own refresh policy.
-- Timer callbacks are registered directly with timer.after/every.
---Required. Runs once before the first draw; failing here stops the app.
---Required. Mounts the route; failing here leaves no app running.
---@param route string The app path sys.launch, sys.back or the boot recorded.
---@param arg? string The string passed to sys.launch or sys.replace.
function init(arg) end
function start(route, arg) end
---Optional frame loop, called once after init and then at most 30 FPS, best effort.
---Optional frame loop, called once after start and then at most 30 FPS, best effort.
---@param deltaMs integer Monotonic milliseconds since the previous draw; zero on the first.
function draw(deltaMs) end
-26
View File
@@ -1,26 +0,0 @@
---@meta
-- Generated from native/src/bindings/core/settings.cpp. Do not edit.
---@class SettingsLib
settings = {}
---Returns the saved rotation in degrees clockwise.
---@return integer
function settings.getRotation() end
---Applies and persists the screen rotation.
---@param degrees integer 0, 90, 180, or 270 clockwise.
---@return true? ok
---@return string? error
function settings.setRotation(degrees) end
---Returns the active POSIX timezone rule.
---@return string
function settings.getTimezone() end
---Applies and persists a POSIX timezone rule.
---@param timezone string
---@return true? ok
---@return string? error
function settings.setTimezone(timezone) end
+21 -7
View File
@@ -2,7 +2,7 @@
-- Generated from native/src/bindings/core/sys.cpp. Do not edit.
---@alias Feature "touch"|"buttons"
---@alias Feature "screen"|"touch"|"buttons"
---@class SysLib
sys = {}
@@ -36,19 +36,23 @@ function sys.getAppDataPath() end
---@param title string
function sys.setAppTitle(title) end
---Launches /.lua/apps/<path>/main.lua and pushes the current route.
---@param path string App-relative directory path; traversal is rejected.
---@param arg? string Passed to init(arg).
---Launches a route, which main.lua resolves, and pushes the current one.
---@param path string App-relative route; traversal is rejected.
---@param arg? string Passed to main.start(route, arg).
function sys.launch(path, arg) end
---Launches an app path without retaining the current route.
---@param path string App-relative directory path; traversal is rejected.
---@param arg? string Passed to init(arg).
---Launches a route without retaining the current one.
---@param path string App-relative route; traversal is rejected.
---@param arg? string Passed to main.start(route, arg).
function sys.replace(path, arg) end
---Returns to the previous app, or the launcher when history is empty.
function sys.back() end
---Whether sys.back() would return somewhere rather than land on the launcher, which is what chrome needs to decide whether to offer a back control.
---@return boolean
function sys.canGoBack() end
---Returns heap statistics.
---@return integer freeBytes
---@return integer totalBytes
@@ -58,3 +62,13 @@ function sys.getMemory() end
---Whether network time synchronization has completed.
---@return boolean
function sys.isClockSynced() end
---Returns the active POSIX timezone rule.
---@return string
function sys.getTimezone() end
---Applies and persists a POSIX timezone rule.
---@param timezone string
---@return true? ok
---@return string? error
function sys.setTimezone(timezone) end
+8 -8
View File
@@ -5,33 +5,33 @@
---@alias Button "up"|"down"|"left"|"right"|"confirm"|"back"
-- Roles, not physical buttons: a device maps whatever hardware it has onto them, and
-- up/down/left/right are the directions node.moveFocus already takes.
-- up/down/left/right are the directions tree.moveFocus already takes.
---@class InputLib
input = input or {}
---@class ButtonsLib
buttons = {}
---Returns the roles this device reports, so an app can label only the actions it has.
---@return Button[]
function input.getButtons() end
function buttons.getAll() end
---Whether any button is held.
---@return boolean
function input.isAnyPressed() end
function buttons.isAnyPressed() end
---Whether a button is held.
---@param button Button
---@return boolean
function input.isPressed(button) end
function buttons.isPressed(button) end
---Whether a button went down since the last poll.
---@param button Button
---@return boolean
function input.wasPressed(button) end
function buttons.wasPressed(button) end
---Whether a button came up since the last poll.
---@param button Button
---@return boolean
function input.wasReleased(button) end
function buttons.wasReleased(button) end
---Fired when a button goes down.
---@param button Button
+164
View File
@@ -0,0 +1,164 @@
---@meta
-- Generated from native/src/bindings/features/screen/screen.cpp. Do not edit.
-- The panel itself; the widget tree it paints is `tree`, and
-- sys.hasFeature("screen") covers both.
---@alias ScreenColor integer
---@alias ScreenFont integer
---@alias ScreenTextStyle integer
---@class ScreenLib
---@field FONT_SMALL ScreenFont Small auxiliary text.
---@field FONT_UI ScreenFont Normal controls and labels.
---@field FONT_BODY ScreenFont Normal reading text.
---@field FONT_LARGE ScreenFont Headings and prominent values.
---@field STYLE_NORMAL ScreenTextStyle
---@field STYLE_BOLD ScreenTextStyle
screen = {}
screen.FONT_SMALL = 0
screen.FONT_UI = 0
screen.FONT_BODY = 0
screen.FONT_LARGE = 0
screen.STYLE_NORMAL = 0
screen.STYLE_BOLD = 0
---Returns the live frame width.
---@return integer
function screen.getWidth() end
---Returns the live frame height.
---@return integer
function screen.getHeight() end
---Rotates the panel and persists the choice, so there is one rotation
---rather than a live one and a saved one to reconcile.
---@param degrees integer 0, 90, 180, or 270 clockwise.
---@return true? ok
---@return string? error
function screen.setRotation(degrees) end
---Returns the rotation in degrees clockwise.
---@return integer
function screen.getRotation() end
---Returns the saved palette name. Apps read ui.getTheme() instead; this
---is the stored value, which only ui.setTheme() knows how to apply.
---@return string
function screen.getTheme() end
---Persists a palette name without applying it. Call ui.setTheme(), which
---writes through here and then rebuilds the palette and repaints.
---@param theme string
---@return true? ok
---@return string? error
function screen.setTheme(theme) end
---Returns an opaque native color. E-ink implementations quantize RGB to available grayscale.
---@param r integer 0 through 255.
---@param g integer 0 through 255.
---@param b integer 0 through 255.
---@return ScreenColor
function screen.color(r, g, b) end
---Clears the frame.
---@param color? ScreenColor Defaults to white.
function screen.clear(color) end
---Fills a rectangle.
---@param x integer
---@param y integer
---@param w integer
---@param h integer
---@param color ScreenColor
function screen.fillRect(x, y, w, h, color) end
---Outlines a rectangle.
---@param x integer
---@param y integer
---@param w integer
---@param h integer
---@param color ScreenColor
function screen.drawRect(x, y, w, h, color) end
---Draws a line.
---@param x1 integer
---@param y1 integer
---@param x2 integer
---@param y2 integer
---@param color ScreenColor
---@param width? integer Defaults to one pixel.
function screen.drawLine(x1, y1, x2, y2, color, width) end
---Draws a single pixel.
---@param x integer
---@param y integer
---@param color ScreenColor
function screen.drawPixel(x, y, color) end
---Outlines a circle.
---@param x integer Center.
---@param y integer Center.
---@param radius integer
---@param color ScreenColor
---@param width? integer Defaults to one pixel.
function screen.drawCircle(x, y, radius, color, width) end
---Fills a circle.
---@param x integer Center.
---@param y integer Center.
---@param radius integer
---@param color ScreenColor
---@param background? ScreenColor Surface behind an anti-aliased edge.
function screen.fillCircle(x, y, radius, color, background) end
---Draws an anti-aliased rounded fill, optional gradient, and optional border in one pass.
---@param x integer
---@param y integer
---@param w integer
---@param h integer
---@param radius integer
---@param background ScreenColor Surface behind the anti-aliased edge.
---@param top? ScreenColor Fill, or gradient top; omitted for no fill.
---@param bottom? ScreenColor Gradient bottom; defaults to top. Panels without a gradient use top.
---@param border? ScreenColor Omitted for no border.
function screen.roundRect(x, y, w, h, radius, background, top, bottom, border) end
---Fills a polygon.
---@param xs integer[]
---@param ys integer[]
---@param color ScreenColor
function screen.fillPolygon(xs, ys, color) end
---Draws a bitmap.
---@param path string Absolute BMP path.
---@param x? integer Left edge; defaults to centered.
---@param y? integer Top edge; defaults to centered.
---@param maxWidth? integer Defaults to panel width.
---@param maxHeight? integer Defaults to panel height.
---@return true? ok
---@return string? error
function screen.drawBmp(path, x, y, maxWidth, maxHeight) end
---Measures a text run.
---@param font ScreenFont Use a named screen.FONT_* role.
---@param text string
---@param style? ScreenTextStyle Defaults to screen.STYLE_NORMAL.
---@return integer
function screen.getTextWidth(font, text, style) end
---Returns the line height of a font role.
---@param font ScreenFont Use a named screen.FONT_* role.
---@param style? ScreenTextStyle Defaults to screen.STYLE_NORMAL.
---@return integer
function screen.getFontHeight(font, style) end
---Draws a text run with its top-left corner at x, y.
---@param font ScreenFont Use a named screen.FONT_* role.
---@param x integer Left edge.
---@param y integer Top edge.
---@param text string
---@param color? ScreenColor Defaults to black.
---@param style? ScreenTextStyle Defaults to screen.STYLE_NORMAL.
---@param background? ScreenColor Omitted for transparent text.
function screen.drawText(font, x, y, text, color, style, background) end
@@ -1,6 +1,6 @@
---@meta
-- Generated from native/src/bindings/core/node.cpp. Do not edit.
-- Generated from native/src/bindings/features/screen/tree.cpp. Do not edit.
---@alias NodeId integer
---@alias NodeType "box"|"text"|"button"|"custom"
@@ -19,43 +19,43 @@
---@field capture? boolean
---@field interactive? boolean
---@field label? string
---@field font? GuiFont
---@field font? ScreenFont
---@class NodeStyle
---@field color? GuiColor
---@field background? GuiColor Background offered to descendants.
---@field fill? GuiColor Surface painted by a box.
---@field border? GuiColor
---@field face? GuiColor Default button surface.
---@field pressedFace? GuiColor Pressed button surface.
---@field pressedColor? GuiColor Pressed button text.
---@field focusColor? GuiColor Distinct outline for directional focus.
---@field color? ScreenColor
---@field background? ScreenColor Background offered to descendants.
---@field fill? ScreenColor Surface painted by a box.
---@field border? ScreenColor
---@field face? ScreenColor Default button surface.
---@field pressedFace? ScreenColor Pressed button surface.
---@field pressedColor? ScreenColor Pressed button text.
---@field focusColor? ScreenColor Distinct outline for directional focus.
---@field radius? integer
---@field font? GuiFont
---@field textStyle? GuiTextStyle
---@field font? ScreenFont
---@field textStyle? ScreenTextStyle
---@class NodeLib
node = {}
---@class TreeLib
tree = {}
---Drops the current tree; all existing IDs become invalid.
function node.reset() end
function tree.reset() end
---Creates a node, optionally as a child of an existing one.
---@param parent? NodeId Nil creates a root.
---@param spec NodeSpec
---@return NodeId
function node.create(parent, spec) end
function tree.create(parent, spec) end
---Adopts an existing root as a child.
---@param parent NodeId
---@param child NodeId Existing root without a parent.
function node.attach(parent, child) end
function tree.attach(parent, child) end
---Changes a node's requested size before layout.
---@param id NodeId
---@param w? number|"fill"|"auto"
---@param h? number|"fill"|"auto"
function node.setSize(id, w, h) end
function tree.setSize(id, w, h) end
---Measures and places a subtree.
---@param root NodeId
@@ -65,17 +65,17 @@ function node.setSize(id, w, h) end
---@param h integer
---@return true? ok
---@return string? error
function node.layout(root, x, y, w, h) end
function tree.layout(root, x, y, w, h) end
---Releases temporary measurement and placement inputs after layout.
function node.dropScratch() end
function tree.dropScratch() end
---Returns the deepest interactive node under a point.
---@param root NodeId
---@param x integer
---@param y integer
---@return NodeId?
function node.hit(root, x, y) end
function tree.hit(root, x, y) end
---Returns a node's placed rectangle.
---@param id NodeId
@@ -83,73 +83,73 @@ function node.hit(root, x, y) end
---@return integer y
---@return integer w
---@return integer h
function node.getRect(id) end
function tree.getRect(id) end
---Replaces a node's text and marks it for repaint.
---@param id NodeId
---@param text string
function node.setLabel(id, text) end
function tree.setLabel(id, text) end
---Returns a node's text.
---@param id NodeId
---@return string?
function node.getLabel(id) end
function tree.getLabel(id) end
---Returns a node's parent.
---@param id NodeId
---@return NodeId?
function node.getParent(id) end
function tree.getParent(id) end
---Sets the style roles a subtree inherits.
---@param id NodeId
---@param style NodeStyle
function node.setStyle(id, style) end
function tree.setStyle(id, style) end
---Marks a node for repaint.
---@param id NodeId
function node.invalidate(id) end
function tree.invalidate(id) end
---Sets a node's pressed state.
---@param id NodeId
---@param pressed boolean
function node.setPressed(id, pressed) end
function tree.setPressed(id, pressed) end
---Whether a node is pressed.
---@param id NodeId
---@return boolean
function node.isPressed(id) end
function tree.isPressed(id) end
---Focuses the first interactive node in layout order.
---@param root NodeId
---@return NodeId? focused
function node.focusFirst(root) end
function tree.focusFirst(root) end
---Changes focus and invalidates the previously and newly focused nodes.
---@param id? NodeId Nil clears focus.
function node.setFocus(id) end
function tree.setFocus(id) end
---Returns the focused node.
---@return NodeId?
function node.getFocus() end
function tree.getFocus() end
---Moves to the nearest interactive node in the requested direction without wrapping.
---@param root NodeId
---@param direction NodeDirection
---@return NodeId? focused Current focus when no candidate exists.
function node.moveFocus(root, direction) end
function tree.moveFocus(root, direction) end
---Registers the painter every custom node calls.
---@param painter fun(id: NodeId, x: integer, y: integer, w: integer, h: integer)
function node.setPainter(painter) end
function tree.setPainter(painter) end
---Paints dirty nodes; the firmware owns publication to the physical display.
---@param root NodeId
function node.draw(root) end
function tree.draw(root) end
---Returns the number of nodes in the tree.
---@return integer
function node.getCount() end
function tree.getCount() end
---Returns the tree's memory use.
---@return integer bytes
function node.getFootprint() end
function tree.getFootprint() end
+17 -20
View File
@@ -2,8 +2,22 @@
-- Generated from native/src/bindings/features/touch.cpp. Do not edit.
---@class SettingsLib
settings = settings or {}
---@class TouchLib
touch = {}
---Returns the calibrated touch point, or nothing when the panel is not touched.
---@return integer? x
---@return integer? y
function touch.getPoint() end
---Returns the uncalibrated touch reading, or nothing when the panel is not touched.
---@return integer? x
---@return integer? y
function touch.getRawPoint() end
---Whether the panel is currently touched.
---@return boolean
function touch.isTouched() end
---Persists the panel's touch calibration.
---@param x0 integer Raw reading at the left edge.
@@ -12,24 +26,7 @@ settings = settings or {}
---@param y1 integer Raw reading at the bottom edge.
---@return true? ok
---@return string? error
function settings.setCalibration(x0, y0, x1, y1) end
---@class InputLib
input = input or {}
---Returns the calibrated touch point, or nothing when the panel is not touched.
---@return integer? x
---@return integer? y
function input.getTouch() end
---Returns the uncalibrated touch reading, or nothing when the panel is not touched.
---@return integer? x
---@return integer? y
function input.getRawTouch() end
---Whether the panel is currently touched.
---@return boolean
function input.isTouched() end
function touch.setCalibration(x0, y0, x1, y1) end
---Fired when the finger lands.
---@param x integer
+11 -11
View File
@@ -8,8 +8,8 @@ local ORDER = { "back", "left", "up", "down", "right", "confirm" }
local function available()
local roles = {}
if input and input.getButtons then
for _, role in ipairs(input.getButtons()) do
if buttons and buttons.getAll then
for _, role in ipairs(buttons.getAll()) do
roles[role] = true
end
end
@@ -21,11 +21,11 @@ end
---@param options table|nil `y`, `font`, `color`, and `background` overrides.
function hints.draw(actions, options)
options = options or {}
local font = options.font or gui.FONT_SMALL
local color = options.color or gui.color(0, 0, 0)
local background = options.background or gui.color(255, 255, 255)
local height = gui.getFontHeight(font) + 6
local y = options.y or (gui.getHeight() - height)
local font = options.font or screen.FONT_SMALL
local color = options.color or screen.color(0, 0, 0)
local background = options.background or screen.color(255, 255, 255)
local height = screen.getFontHeight(font) + 6
local y = options.y or (screen.getHeight() - height)
local roles = available()
local labels = {}
@@ -35,15 +35,15 @@ function hints.draw(actions, options)
end
end
gui.fillRect(0, y, gui.getWidth(), height, background)
screen.fillRect(0, y, screen.getWidth(), height, background)
if #labels == 0 then
return height
end
local slot = gui.getWidth() // #labels
local slot = screen.getWidth() // #labels
for index, label in ipairs(labels) do
local left = slot * (index - 1) + (slot - gui.getTextWidth(font, label)) // 2
gui.drawText(font, left, y + 3, label, color, gui.STYLE_NORMAL, background)
local left = slot * (index - 1) + (slot - screen.getTextWidth(font, label)) // 2
screen.drawText(font, left, y + 3, label, color, screen.STYLE_NORMAL, background)
end
return height
end
+106 -87
View File
@@ -42,7 +42,6 @@ local ui = {}
---@field on_cancel? UiHandler
---@field on_outside? UiHandler
local THEME_PATH = "/.lua/theme"
local THEMES = {
light = { background = { 255, 255, 255 }, color = { 0, 0, 0 }, accent = { 0, 120, 255 }, radius = 6 },
dark = { background = { 18, 18, 20 }, color = { 235, 235, 235 }, accent = { 166, 118, 255 }, radius = 6 },
@@ -55,7 +54,10 @@ local enterHandlers, exitHandlers, clickHandlers, painters = {}, {}, {}, {}
local pressStyles = {}
local laidOut = false
local themeName
local activeScreen
-- One tree per state, built by the function ui.mount() was given. Rebuilding is
-- cheap enough that nothing is retained between screens.
local builder, root, captured, insideCaptured, confirming
local inset = 0
local applyPalette
local function mix(a, b, amount)
@@ -67,7 +69,7 @@ local function mix(a, b, amount)
end
local function color(rgb)
return gui.color(rgb[1], rgb[2], rgb[3])
return screen.color(rgb[1], rgb[2], rgb[3])
end
local function palette(seed)
@@ -113,28 +115,27 @@ function ui.setTheme(name)
if not THEMES[name] then
return nil, "Unknown theme"
end
local ok, err = fs.writeFile(THEME_PATH, name)
local ok, err = screen.setTheme(name)
if not ok then
return nil, err
end
loadTheme(name)
if activeScreen then
applyPalette(activeScreen.root)
gui.clear(ui.theme.background)
node.invalidate(activeScreen.root)
if root then
applyPalette(root)
screen.clear(ui.theme.background)
tree.invalidate(root)
end
return true
end
local savedTheme = fs.readFile(THEME_PATH, 32)
loadTheme(savedTheme and savedTheme:match "^%s*(.-)%s*$" or "light")
loadTheme(screen.getTheme())
local function clearState()
enterHandlers, exitHandlers, clickHandlers, painters = {}, {}, {}, {}
pressStyles = {}
end
node.setPainter(function(id, x, y, w, h)
tree.setPainter(function(id, x, y, w, h)
local painter = painters[id]
if painter then
painter(id, x, y, w, h)
@@ -165,14 +166,16 @@ local function applyStyle(id, spec)
style.background, style.fill, hasStyle = spec.background, spec.background, true
end
if hasStyle then
node.setStyle(id, style)
tree.setStyle(id, style)
end
end
local function build(spec, kind)
spec = spec or {}
-- Nodes outside a build would reset the arena under the screen already on the
-- panel, chrome included. Rebuilding is the only way to change one.
if laidOut then
ui.reset()
error("build nodes from the function ui.mount() was given, then ui.rebuild()", 3)
end
local children = {}
@@ -183,9 +186,9 @@ local function build(spec, kind)
spec.type = kind
spec.interactive = spec.on_enter ~= nil or spec.on_exit ~= nil or spec.on_click ~= nil
local id = node.create(nil, spec)
local id = tree.create(nil, spec)
for _, child in ipairs(children) do
node.attach(id, child)
tree.attach(id, child)
end
applyStyle(id, spec)
@@ -212,13 +215,28 @@ end
---@return integer side
---@return integer columns
function ui.cardSide(count, pad, gap, reserve)
local columns = gui.getWidth() >= gui.getHeight() and 3 or 2
local width, height = ui.frame()
local columns = width >= height and 3 or 2
local rows = math.ceil(count / columns)
local byWidth = (gui.getWidth() - 2 * pad - (columns - 1) * gap) // columns
local byHeight = (gui.getHeight() - 2 * pad - (reserve or 0) - (rows - 1) * gap) // rows
local byWidth = (width - 2 * pad - (columns - 1) * gap) // columns
local byHeight = (height - 2 * pad - (reserve or 0) - (rows - 1) * gap) // rows
return math.min(byWidth, byHeight), columns
end
---How much of the panel chrome took before the app was built. Set by whatever mounts the
---tree, because layout has not run yet when an app sizes itself.
---@param px integer
function ui.setInset(px)
inset = px
end
---The box the app is built into, which is the panel minus the chrome above it.
---@return integer w
---@return integer h
function ui.frame()
return screen.getWidth(), screen.getHeight() - inset
end
---@param spec UiSpec
---@return NodeId
function ui.spacer(spec)
@@ -241,15 +259,15 @@ end
---@return NodeId
function ui.label(text, spec)
spec = spec or {}
local font, style = spec.font or gui.FONT_UI, spec.style or gui.STYLE_NORMAL
if spec.fit and gui.getTextWidth(font, text, style) > spec.fit then
while #text > 1 and gui.getTextWidth(font, text .. "~", style) > spec.fit do
local font, style = spec.font or screen.FONT_UI, spec.style or screen.STYLE_NORMAL
if spec.fit and screen.getTextWidth(font, text, style) > spec.fit then
while #text > 1 and screen.getTextWidth(font, text .. "~", style) > spec.fit do
text = text:sub(1, -2)
end
text = text .. "~"
end
spec.w = gui.getTextWidth(font, text, style)
spec.h = gui.getFontHeight(font, style)
spec.w = screen.getTextWidth(font, text, style)
spec.h = screen.getFontHeight(font, style)
spec.font, spec.fit = font, nil
return ui.text(text, spec)
end
@@ -264,7 +282,7 @@ function ui.button(spec)
spec.label = nil
local id = build(spec, "button")
if label then
node.create(id, { type = "text", label = label, font = font or gui.FONT_UI })
tree.create(id, { type = "text", label = label, font = font or screen.FONT_UI })
end
return id
end
@@ -278,16 +296,16 @@ end
---@param id NodeId
---@param text string
function ui.setText(id, text)
if node.getLabel(id) == text then
if tree.getLabel(id) == text then
return
end
node.setLabel(id, text)
node.invalidate(id)
tree.setLabel(id, text)
tree.invalidate(id)
end
---@param id NodeId
function ui.invalidate(id)
node.invalidate(id)
tree.invalidate(id)
end
---@param spec UiConfirmSpec
@@ -325,20 +343,16 @@ function ui.confirm(spec)
end
function ui.reset()
node.reset()
tree.reset()
clearState()
laidOut = false
activeScreen = nil
root, captured, insideCaptured, confirming = nil, nil, nil, nil
end
---@class UiScreen
local Screen = {}
Screen.__index = Screen
applyPalette = function(root)
-- The root is the panel background, not a card: no border, so it takes the fast fillRect
-- path rather than the per-pixel roundRect one. Radius stays so cards inherit it.
node.setStyle(root, {
tree.setStyle(root, {
color = ui.theme.color,
background = ui.theme.background,
face = ui.theme.face,
@@ -346,47 +360,53 @@ applyPalette = function(root)
pressedColor = ui.theme.pressedColor,
focusColor = ui.theme.focusColor,
radius = ui.theme.radius,
font = gui.FONT_UI,
font = screen.FONT_UI,
})
end
---@param root NodeId
---@param style? NodeStyle
---@return UiScreen
function ui.screen(root, style)
node.setSize(root, "fill", "fill")
applyPalette(root)
if style then
node.setStyle(root, style)
end
local screen = setmetatable({ root = root }, Screen)
activeScreen = screen
screen:relayout()
return screen
---Registers the function that builds the whole tree and shows what it returns.
---@param fn fun(): NodeId
function ui.mount(fn)
builder = fn
ui.rebuild()
end
function Screen:relayout()
local ok, err = node.layout(self.root, 0, 0, gui.getWidth(), gui.getHeight())
---Rebuilds the tree from scratch and repaints. Screens are not retained, so this
---is how a screen changes, a rotation is answered and a dialog opens.
function ui.rebuild()
ui.reset()
root = builder()
tree.setSize(root, "fill", "fill")
applyPalette(root)
local ok, err = tree.layout(root, 0, 0, screen.getWidth(), screen.getHeight())
if not ok then
error(err, 2)
end
node.dropScratch()
tree.dropScratch()
laidOut = true
gui.clear(ui.theme.background)
screen.clear(ui.theme.background)
tree.draw(root)
-- A build allocates a spec table per node and drops them all here, and the next thing an
-- app does may be the one that needs a contiguous WiFi buffer. Collecting now costs a few
-- milliseconds on a screen change nobody can see, and leaves the heap in a known state
-- instead of one that depends on when the incremental GC last ran.
collectgarbage()
end
function Screen:draw()
node.draw(self.root)
function ui.draw()
if root then
tree.draw(root)
end
end
local function inside(id, x, y)
local rx, ry, rw, rh = node.getRect(id)
local rx, ry, rw, rh = tree.getRect(id)
return x >= rx and x < rx + rw and y >= ry and y < ry + rh
end
local function enter(id, x, y)
if pressStyles[id] then
node.setPressed(id, true)
tree.setPressed(id, true)
end
local handler = enterHandlers[id]
if handler then
@@ -396,7 +416,7 @@ end
local function exit(id, x, y)
if pressStyles[id] then
node.setPressed(id, false)
tree.setPressed(id, false)
end
local handler = exitHandlers[id]
if handler then
@@ -407,21 +427,21 @@ end
---@param x integer
---@param y integer
---@return boolean handled
function Screen:down(x, y)
local focused = node.getFocus()
function ui.down(x, y)
local focused = tree.getFocus()
if focused then
node.setFocus(nil)
tree.setFocus(nil)
local handler = exitHandlers[focused]
if handler then
handler(focused)
end
end
local target = node.hit(self.root, x, y)
local target = root and tree.hit(root, x, y)
if not target then
return false
end
self.captured, self.inside = target, true
captured, insideCaptured = target, true
enter(target, x, y)
return true
end
@@ -429,18 +449,17 @@ end
---@param x integer
---@param y integer
---@return boolean handled
function Screen:move(x, y)
local target = self.captured
if not target then
function ui.move(x, y)
if not captured then
return false
end
local isInside = inside(target, x, y)
if isInside ~= self.inside then
self.inside = isInside
local isInside = inside(captured, x, y)
if isInside ~= insideCaptured then
insideCaptured = isInside
if isInside then
enter(target, x, y)
enter(captured, x, y)
else
exit(target, x, y)
exit(captured, x, y)
end
end
return true
@@ -449,15 +468,15 @@ end
---@param x integer
---@param y integer
---@return boolean handled
function Screen:up(x, y)
local target = self.captured
function ui.up(x, y)
local target = captured
if not target then
return false
end
local wasActive = self.inside
local wasActive = insideCaptured
local releasedInside = inside(target, x, y)
local handler = releasedInside and clickHandlers[target] or nil
self.captured, self.inside = nil, nil
captured, insideCaptured = nil, nil
if wasActive then
exit(target, x, y)
end
@@ -469,8 +488,8 @@ end
local DIRECTIONS = { up = true, down = true, left = true, right = true }
local function focusFirst(screen)
local focused = node.focusFirst(screen.root)
local function focusFirst()
local focused = tree.focusFirst(root)
if focused then
local handler = enterHandlers[focused]
if handler then
@@ -483,7 +502,7 @@ end
---@param name string Button name; directions and confirm are handled.
---@param pressed boolean
---@return boolean handled
function Screen:button(name, pressed)
function ui.buttonPress(name, pressed)
if type(pressed) ~= "boolean" then
error("button state must be boolean", 2)
end
@@ -492,12 +511,12 @@ function Screen:button(name, pressed)
if not pressed then
return true
end
local previous = node.getFocus()
local previous = tree.getFocus()
if not previous then
focusFirst(self)
focusFirst()
return true
end
local focused = node.moveFocus(self.root, name)
local focused = tree.moveFocus(root, name)
if focused ~= previous then
local leave = exitHandlers[previous]
if leave then
@@ -514,18 +533,18 @@ function Screen:button(name, pressed)
if name ~= "confirm" then
return false
end
local focused = node.getFocus() or focusFirst(self)
local focused = tree.getFocus() or focusFirst()
if not focused then
return false
end
if pressed then
node.setPressed(focused, true)
self.confirming = focused
tree.setPressed(focused, true)
confirming = focused
else
local target = self.confirming
self.confirming = nil
local target = confirming
confirming = nil
if target then
node.setPressed(target, false)
tree.setPressed(target, false)
local handler = clickHandlers[target]
if handler then
handler(target)
+3 -3
View File
@@ -3,7 +3,7 @@ package.path = "./lua/lib/?.lua;" .. package.path
local drawn = {}
local roles = { "confirm", "back", "right" }
gui = {
screen = {
FONT_SMALL = 0,
STYLE_NORMAL = 0,
color = function(r, g, b)
@@ -29,8 +29,8 @@ gui = {
end,
}
input = {
getButtons = function()
buttons = {
getAll = function()
return roles
end,
}
+71 -43
View File
@@ -17,9 +17,17 @@ fs = {
end,
}
local savedTheme = "light"
local frameWidth, frameHeight = 320, 480
gui = {
screen = {
getTheme = function()
return savedTheme
end,
setTheme = function(name)
savedTheme = name
return true
end,
FONT_SMALL = 0,
FONT_UI = 1,
FONT_BODY = 2,
@@ -59,7 +67,7 @@ local function interactiveNodes()
return result
end
node = {
tree = {
reset = function()
nodes, focus, buttonCount = {}, nil, 0
end,
@@ -166,7 +174,7 @@ assert(table.concat(ui.themeNames(), ",") == "dark,light,mono")
local ok, err = ui.setTheme "missing"
assert(ok == nil and err == "Unknown theme")
assert(ui.setTheme "dark" == true)
assert(files["/.lua/theme"] == "dark" and ui.getTheme() == "dark")
assert(savedTheme == "dark" and ui.getTheme() == "dark")
local events = {}
local function handler(name)
@@ -175,28 +183,31 @@ local function handler(name)
end
end
local first = ui.button {
label = "one",
on_enter = handler "enter",
on_exit = handler "exit",
on_click = handler "click",
}
local second = ui.button {
label = "two",
on_enter = handler "enter",
on_exit = handler "exit",
on_click = handler "click",
}
local screen = ui.screen(ui.box { row = true, first, second })
local first, second
ui.mount(function()
first = ui.button {
label = "one",
on_enter = handler "enter",
on_exit = handler "exit",
on_click = handler "click",
}
second = ui.button {
label = "two",
on_enter = handler "enter",
on_exit = handler "exit",
on_click = handler "click",
}
return ui.box { row = true, first, second }
end)
assert(screen:down(10, 10))
assert(node.isPressed(first))
assert(screen:move(95, 10))
assert(not node.isPressed(first))
assert(screen:move(10, 10))
assert(node.isPressed(first))
assert(screen:up(10, 10))
assert(not node.isPressed(first))
assert(ui.down(10, 10))
assert(tree.isPressed(first))
assert(ui.move(95, 10))
assert(not tree.isPressed(first))
assert(ui.move(10, 10))
assert(tree.isPressed(first))
assert(ui.up(10, 10))
assert(not tree.isPressed(first))
local expectedTouch = { "enter", "exit", "enter", "exit", "click" }
for index, name in ipairs(expectedTouch) do
@@ -207,16 +218,16 @@ for index, name in ipairs(expectedTouch) do
end
events = {}
assert(screen:button("right", true))
assert(screen:button("right", false))
assert(node.getFocus() == first)
assert(screen:button("right", true))
assert(node.getFocus() == second)
assert(screen:button("confirm", true))
assert(node.isPressed(second))
assert(screen:button("confirm", false))
assert(not node.isPressed(second))
assert(screen:button("back", true) == false)
assert(ui.buttonPress("right", true))
assert(ui.buttonPress("right", false))
assert(tree.getFocus() == first)
assert(ui.buttonPress("right", true))
assert(tree.getFocus() == second)
assert(ui.buttonPress("confirm", true))
assert(tree.isPressed(second))
assert(ui.buttonPress("confirm", false))
assert(not tree.isPressed(second))
assert(ui.buttonPress("back", true) == false)
local expectedButtons = {
{ "enter", first },
@@ -235,7 +246,13 @@ assert(ui.setTheme "mono" == true)
assert(ui.getTheme() == "mono" and #invalidated == before + 1)
assert(cleared == ui.theme.background)
screen:draw()
ui.draw()
-- The inset chrome took comes off the height budget before anything else.
ui.setInset(44)
assert(select(2, ui.frame()) == 436, "the frame is the panel minus the chrome")
assert(select(1, ui.cardSide(5, 12, 8)) == 132, "a grid fits the app's box, not the panel")
ui.setInset(0)
-- 320x480 portrait: two columns, and the reserve comes off the height budget.
local side, columns = ui.cardSide(5, 12, 8)
@@ -251,24 +268,35 @@ frameWidth, frameHeight = 320, 480
-- still is. Which node was hit is geometry, so the test names the target directly.
ui.reset()
local pressedCalls = {}
node.setPressed = function(_, on)
tree.setPressed = function(_, on)
pressedCalls[#pressedCalls + 1] = on
end
local own = ui.custom { h = 20, press_style = false, on_click = function() end }
local styled = ui.button { h = 20, label = "ok", on_click = function() end }
local board = ui.screen(ui.box { own, styled })
local own, styled
ui.mount(function()
own = ui.custom { h = 20, press_style = false, on_click = function() end }
styled = ui.button { h = 20, label = "ok", on_click = function() end }
return ui.box { own, styled }
end)
local target
node.hit = function()
tree.hit = function()
return target
end
target = own
board:down(0, 0)
board:up(0, 0)
ui.down(0, 0)
ui.up(0, 0)
assert(#pressedCalls == 0, "a self-painting widget is not styled on press")
target = styled
board:down(0, 0)
ui.down(0, 0)
assert(pressedCalls[1] == true, "an ordinary widget still gets its pressed style")
-- Building outside a rebuild would reset the arena under the screen on the panel.
local built = pcall(ui.button, { label = "stray" })
assert(not built, "a node built after layout is refused")
local rebuilt = false
ui.rebuild()
rebuilt = true
assert(rebuilt and ui.down(0, 0), "a rebuild replaces the screen and keeps dispatch live")
print "ok"
+9 -13
View File
@@ -35,15 +35,6 @@ struct MemoryInfo {
int32_t largestFreeBlock;
};
class SettingsProvider {
public:
virtual ~SettingsProvider() = default;
virtual int32_t rotation() const = 0;
virtual Status setRotation(int32_t degrees) = 0;
virtual std::string timezone() const = 0;
virtual Status setTimezone(const std::string& timezone) = 0;
};
// Only what the firmware alone can answer. App identity, titles, data paths,
// feature reporting and navigation are the runtime's, because it owns app
// loading and knows which providers exist.
@@ -53,6 +44,8 @@ public:
virtual int32_t millis() const = 0;
virtual MemoryInfo memory() const = 0;
virtual bool isClockSynced() const = 0;
virtual std::string timezone() const = 0;
virtual Status setTimezone(const std::string& timezone) = 0;
};
// Scripts are streamed rather than slurped: a whole module in one buffer needs
@@ -111,7 +104,13 @@ public:
virtual int32_t width() const = 0;
virtual int32_t height() const = 0;
virtual int32_t rotation() const = 0;
virtual void setRotation(int32_t degrees) = 0;
// Applies the rotation and persists it, so the panel comes back the way the
// user left it with no second call to forget.
virtual Status setRotation(int32_t degrees) = 0;
// Only the name of a palette; the colours themselves live in Lua, so the
// firmware can read the saved theme before a lua_State exists.
virtual std::string theme() const = 0;
virtual Status setTheme(const std::string& theme) = 0;
virtual int32_t color(int32_t r, int32_t g, int32_t b) const = 0;
virtual void clear(int32_t color) = 0;
virtual void fillRect(int32_t x, int32_t y, int32_t w, int32_t h,
@@ -132,9 +131,6 @@ public:
virtual void roundRect(int32_t x, int32_t y, int32_t w, int32_t h,
int32_t radius, int32_t background, const int32_t* top,
const int32_t* bottom, const int32_t* border) = 0;
// Hands the app the whole panel, including whatever chrome the firmware
// paints.
virtual void setFullscreen(bool on) = 0;
// Applies everything drawn since the last commit. The runtime supplies only
// the timing -- the end of a callback batch -- because that is the one fact a
// driver cannot know; which region to touch, which waveform, and whether to
+28 -23
View File
@@ -15,36 +15,31 @@ namespace esp32lua {
// build.
constexpr int32_t API_VERSION = 1;
// Where the runtime looks for apps, their data, and shared modules.
struct Paths {
std::string apps = "/.lua/apps";
std::string data = "/.lua/data";
std::string lib = "/.lua/lib";
// Where sys.back() lands once history is empty. It is an app like any other.
std::string home = "Home";
};
// The one path the firmware knows. Everything below it -- where apps live,
// where their data goes, what chrome surrounds them -- is decided by the table
// this file returns.
constexpr const char* MAIN_PATH = "/.lua/main.lua";
// Firmware supplies every core provider; a null feature provider is how
// sys.hasFeature() answers false, and its namespace additions are simply never
// registered.
struct Providers {
LogProvider* log = nullptr;
SettingsProvider* settings = nullptr;
SysProvider* sys = nullptr;
FsProvider* fs = nullptr;
GuiProvider* gui = nullptr;
HttpProvider* http = nullptr;
TimerProvider* timer = nullptr;
WifiProvider* wifi = nullptr;
BleProvider* ble = nullptr;
GuiProvider* gui = nullptr;
TouchProvider* touch = nullptr;
ButtonsProvider* buttons = nullptr;
};
class Runtime {
public:
explicit Runtime(const Providers& providers, const Paths& paths = Paths());
explicit Runtime(const Providers& providers);
~Runtime();
Runtime(const Runtime&) = delete;
@@ -55,9 +50,9 @@ public:
void close();
lua_State* state() const { return state_; }
// Replaces the running app with a fresh lua_State, loads
// <apps>/<path>/main.lua, and calls init(arg). A failure leaves no app
// running rather than a half-built one.
// Replaces the running app with a fresh lua_State, loads main.lua, and hands
// it the route through start(route, arg). A failure leaves no app running
// rather than a half-built one.
bool startApp(const std::string& path,
const std::string& arg = std::string());
bool hasApp() const { return !appPath_.empty(); }
@@ -86,7 +81,6 @@ public:
bool applyPendingNavigation();
LogProvider& log() const { return *providers_.log; }
SettingsProvider& settings() const { return *providers_.settings; }
SysProvider& sys() const { return *providers_.sys; }
FsProvider& fs() const { return *providers_.fs; }
GuiProvider& gui() const { return *providers_.gui; }
@@ -99,11 +93,11 @@ public:
ui::Tree& tree() { return tree_; }
// Entry points into the app. The firmware decides whether an event reaches
// the app at all -- jitter, chrome and debouncing are its business -- and the
// runtime decides what the app sees. Only a failed init() stops an app; every
// other callback logs and carries on.
bool callInit(const std::string& arg);
// Entry points into main.lua, which forwards whatever the app it mounted
// defines. The firmware decides whether an event happens at all -- jitter and
// debouncing are its business -- and main.lua decides who sees it. Only a
// failed start() stops an app; every other callback logs and carries on.
bool callStart(const std::string& route, const std::string& arg);
void callDraw(int32_t deltaMs);
// An Up phase also fires the on_touch tap alias, in that order.
void callTouch(TouchPhase phase, int32_t x, int32_t y);
@@ -136,7 +130,12 @@ private:
};
bool loadScript(const std::string& path);
void installLoader(const std::string& appDir);
// Runs main.lua and keeps the table it returns; the app is mounted by it, not
// by the runtime.
bool loadMain();
void installLoader();
// A field of the main table, or the fallback when main.lua names none.
std::string mainField(const char* key, const char* fallback);
static int searchModule(lua_State* state);
static int searchEmbedded(lua_State* state);
static int loadFile(lua_State* state);
@@ -155,9 +154,10 @@ private:
Runtime& runtime_;
};
// Pushes the named global, or returns false when the app does not define it.
// Pushes main.<name>, or returns false when main.lua defines no such handler.
bool beginCall(const char* name);
bool finishCall(const char* name, int argc);
bool finishCallValue(const char* name);
void cancelAllTimers();
Providers providers_;
@@ -167,7 +167,12 @@ private:
TimerId nextTimerId_ = 1;
int batchDepth_ = 0;
Paths paths_;
// Registry reference to the table main.lua returned, or 0 before one loads.
int mainRef_ = 0;
// Read from main.lua once per load, because sys.back() out of the last app
// needs the route after that app's state is gone.
std::string home_;
std::string dataTemplate_;
std::string appPath_;
std::string appTitle_;
std::vector<Route> history_;
-78
View File
@@ -1,78 +0,0 @@
// @lua-module settings SettingsLib
#include <lua/runtime.h>
extern "C" {
#include "lauxlib.h"
#include "lua.h"
}
namespace esp32lua {
namespace bindings {
namespace {
int pushStatus(lua_State* state, const Status& status) {
if (status.ok) {
lua_pushboolean(state, true);
return 1;
}
lua_pushnil(state);
lua_pushlstring(state, status.error.data(), status.error.size());
return 2;
}
int getRotation(lua_State* state) {
lua_pushinteger(state, Runtime::from(state)->settings().rotation());
return 1;
}
int setRotation(lua_State* state) {
const lua_Integer degrees = luaL_checkinteger(state, 1);
luaL_argcheck(state, degrees >= 0 && degrees <= 270 && degrees % 90 == 0, 1,
"expected 0, 90, 180, or 270");
return pushStatus(state,
Runtime::from(state)->settings().setRotation(degrees));
}
int getTimezone(lua_State* state) {
const std::string timezone = Runtime::from(state)->settings().timezone();
lua_pushlstring(state, timezone.data(), timezone.size());
return 1;
}
int setTimezone(lua_State* state) {
size_t length = 0;
const char* value = luaL_checklstring(state, 1, &length);
return pushStatus(
state, Runtime::from(state)->settings().setTimezone({value, length}));
}
const luaL_Reg FUNCTIONS[] = {
// --- Returns the saved rotation in degrees clockwise.
// @return integer
{"getRotation", getRotation},
// --- Applies and persists the screen rotation.
// @param degrees integer 0, 90, 180, or 270 clockwise.
// @return true|nil ok
// @return string|nil error
{"setRotation", setRotation},
// --- Returns the active POSIX timezone rule.
// @return string
{"getTimezone", getTimezone},
// --- Applies and persists a POSIX timezone rule.
// @param timezone string
// @return true|nil ok
// @return string|nil error
{"setTimezone", setTimezone},
{nullptr, nullptr},
};
} // namespace
void registerSettings(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
lua_setglobal(state, "settings");
}
} // namespace bindings
} // namespace esp32lua
+33 -7
View File
@@ -1,5 +1,5 @@
// @lua-module sys SysLib
// @lua-preamble ---@alias Feature "touch"|"buttons"
// @lua-preamble ---@alias Feature "screen"|"touch"|"buttons"
#include "../helpers.h"
@@ -50,6 +50,10 @@ int back(lua_State* state) {
Runtime::from(state)->requestBack();
return 0;
}
int canGoBack(lua_State* state) {
lua_pushboolean(state, Runtime::from(state)->canGoBack());
return 1;
}
int getMemory(lua_State* state) {
const MemoryInfo memory = Runtime::from(state)->sys().memory();
@@ -62,6 +66,14 @@ int isClockSynced(lua_State* state) {
lua_pushboolean(state, Runtime::from(state)->sys().isClockSynced());
return 1;
}
int getTimezone(lua_State* state) {
pushString(state, Runtime::from(state)->sys().timezone());
return 1;
}
int setTimezone(lua_State* state) {
const std::string timezone = checkString(state, 1);
return pushStatus(state, Runtime::from(state)->sys().setTimezone(timezone));
}
const luaL_Reg FUNCTIONS[] = {
// --- Returns the implemented API contract version.
@@ -88,16 +100,22 @@ const luaL_Reg FUNCTIONS[] = {
// --- Changes the running app's display title.
// @param title string
{"setAppTitle", setAppTitle},
// --- Launches /.lua/apps/<path>/main.lua and pushes the current route.
// @param path string App-relative directory path; traversal is rejected.
// @param arg string|nil Passed to init(arg).
// --- Launches a route, which main.lua resolves, and pushes the current
// one.
// @param path string App-relative route; traversal is rejected.
// @param arg string|nil Passed to main.start(route, arg).
{"launch", launch},
// --- Launches an app path without retaining the current route.
// @param path string App-relative directory path; traversal is rejected.
// @param arg string|nil Passed to init(arg).
// --- Launches a route without retaining the current one.
// @param path string App-relative route; traversal is rejected.
// @param arg string|nil Passed to main.start(route, arg).
{"replace", replace},
// --- Returns to the previous app, or the launcher when history is empty.
{"back", back},
// --- Whether sys.back() would return somewhere rather than land on the
// launcher, which is what chrome needs to decide whether to offer a back
// control.
// @return boolean
{"canGoBack", canGoBack},
// --- Returns heap statistics.
// @return integer freeBytes
// @return integer totalBytes
@@ -106,6 +124,14 @@ const luaL_Reg FUNCTIONS[] = {
// --- Whether network time synchronization has completed.
// @return boolean
{"isClockSynced", isClockSynced},
// --- Returns the active POSIX timezone rule.
// @return string
{"getTimezone", getTimezone},
// --- Applies and persists a POSIX timezone rule.
// @param timezone string
// @return true|nil ok
// @return string|nil error
{"setTimezone", setTimezone},
{nullptr, nullptr},
};
+6 -5
View File
@@ -32,18 +32,18 @@ int wasReleased(lua_State* state) {
return 1;
}
// @lua-augment input InputLib
// @lua-module buttons ButtonsLib
// @lua-preamble ---@alias Button "up"|"down"|"left"|"right"|"confirm"|"back"
// @lua-preamble
// @lua-preamble -- Roles, not physical buttons: a device maps whatever hardware
// it has onto them, and
// @lua-preamble -- up/down/left/right are the directions node.moveFocus already
// @lua-preamble -- up/down/left/right are the directions tree.moveFocus already
// takes.
const luaL_Reg INPUT_FUNCTIONS[] = {
const luaL_Reg FUNCTIONS[] = {
// ---Returns the roles this device reports, so an app can label only the
// actions it has.
// @return Button[]
{"getButtons", getButtons},
{"getAll", getButtons},
// ---Whether any button is held.
// @return boolean
{"isAnyPressed", isAnyPressed},
@@ -65,7 +65,8 @@ const luaL_Reg INPUT_FUNCTIONS[] = {
} // namespace
void registerButtons(lua_State* state) {
augmentGlobal(state, "input", INPUT_FUNCTIONS);
luaL_newlib(state, FUNCTIONS);
lua_setglobal(state, "buttons");
}
} // namespace bindings
@@ -1,15 +1,17 @@
// @lua-module gui GuiLib
// @lua-preamble ---@alias GuiColor integer
// @lua-preamble ---@alias GuiFont integer
// @lua-preamble ---@alias GuiTextStyle integer
// @lua-const FONT_SMALL GuiFont 0 Small auxiliary text.
// @lua-const FONT_UI GuiFont 0 Normal controls and labels.
// @lua-const FONT_BODY GuiFont 0 Normal reading text.
// @lua-const FONT_LARGE GuiFont 0 Headings and prominent values.
// @lua-const STYLE_NORMAL GuiTextStyle 0
// @lua-const STYLE_BOLD GuiTextStyle 0
// @lua-module screen ScreenLib
// @lua-preamble -- The panel itself; the widget tree it paints is `tree`, and
// @lua-preamble -- sys.hasFeature("screen") covers both.
// @lua-preamble ---@alias ScreenColor integer
// @lua-preamble ---@alias ScreenFont integer
// @lua-preamble ---@alias ScreenTextStyle integer
// @lua-const FONT_SMALL ScreenFont 0 Small auxiliary text.
// @lua-const FONT_UI ScreenFont 0 Normal controls and labels.
// @lua-const FONT_BODY ScreenFont 0 Normal reading text.
// @lua-const FONT_LARGE ScreenFont 0 Headings and prominent values.
// @lua-const STYLE_NORMAL ScreenTextStyle 0
// @lua-const STYLE_BOLD ScreenTextStyle 0
#include "../helpers.h"
#include "../../helpers.h"
namespace esp32lua {
namespace bindings {
@@ -36,8 +38,17 @@ int setRotation(lua_State* state) {
const int32_t degrees = checkInt(state, 1);
luaL_argcheck(state, degrees >= 0 && degrees <= 270 && degrees % 90 == 0, 1,
"expected 0, 90, 180, or 270");
provider(state).setRotation(degrees);
return 0;
return pushStatus(state, provider(state).setRotation(degrees));
}
int getTheme(lua_State* state) {
pushString(state, provider(state).theme());
return 1;
}
int setTheme(lua_State* state) {
const std::string theme = checkString(state, 1);
return pushStatus(state, provider(state).setTheme(theme));
}
int color(lua_State* state) {
@@ -125,12 +136,6 @@ int roundRect(lua_State* state) {
return 0;
}
int setFullscreen(lua_State* state) {
luaL_checkany(state, 1);
provider(state).setFullscreen(lua_toboolean(state, 1) != 0);
return 0;
}
void readIntegers(lua_State* state, int index, std::vector<int32_t>& out) {
const lua_Integer count = luaL_len(state, index);
for (lua_Integer at = 1; at <= count; at++) {
@@ -221,62 +226,76 @@ const luaL_Reg FUNCTIONS[] = {
// --- Returns the live frame height.
// @return integer
{"getHeight", getHeight},
// --- Rotates the live frame without changing the saved preference.
// --- Rotates the panel and persists the choice, so there is one rotation
// --- rather than a live one and a saved one to reconcile.
// @param degrees integer 0, 90, 180, or 270 clockwise.
// @return true|nil ok
// @return string|nil error
{"setRotation", setRotation},
// --- Returns the rotation of the live frame.
// @return integer Degrees clockwise for the live frame.
// --- Returns the rotation in degrees clockwise.
// @return integer
{"getRotation", getRotation},
// --- Returns the saved palette name. Apps read ui.getTheme() instead; this
// --- is the stored value, which only ui.setTheme() knows how to apply.
// @return string
{"getTheme", getTheme},
// --- Persists a palette name without applying it. Call ui.setTheme(),
// which
// --- writes through here and then rebuilds the palette and repaints.
// @param theme string
// @return true|nil ok
// @return string|nil error
{"setTheme", setTheme},
// --- Returns an opaque native color. E-ink implementations quantize RGB to
// available grayscale.
// @param r integer 0 through 255.
// @param g integer 0 through 255.
// @param b integer 0 through 255.
// @return GuiColor
// @return ScreenColor
{"color", color},
// --- Clears the frame.
// @param color GuiColor|nil Defaults to white.
// @param color ScreenColor|nil Defaults to white.
{"clear", clear},
// --- Fills a rectangle.
// @param x integer
// @param y integer
// @param w integer
// @param h integer
// @param color GuiColor
// @param color ScreenColor
{"fillRect", fillRect},
// --- Outlines a rectangle.
// @param x integer
// @param y integer
// @param w integer
// @param h integer
// @param color GuiColor
// @param color ScreenColor
{"drawRect", drawRect},
// --- Draws a line.
// @param x1 integer
// @param y1 integer
// @param x2 integer
// @param y2 integer
// @param color GuiColor
// @param color ScreenColor
// @param width integer|nil Defaults to one pixel.
{"drawLine", drawLine},
// --- Draws a single pixel.
// @param x integer
// @param y integer
// @param color GuiColor
// @param color ScreenColor
{"drawPixel", drawPixel},
// --- Outlines a circle.
// @param x integer Center.
// @param y integer Center.
// @param radius integer
// @param color GuiColor
// @param color ScreenColor
// @param width integer|nil Defaults to one pixel.
{"drawCircle", drawCircle},
// --- Fills a circle.
// @param x integer Center.
// @param y integer Center.
// @param radius integer
// @param color GuiColor
// @param background GuiColor|nil Surface behind an anti-aliased edge.
// @param color ScreenColor
// @param background ScreenColor|nil Surface behind an anti-aliased edge.
{"fillCircle", fillCircle},
// ---Draws an anti-aliased rounded fill, optional gradient, and optional
// border in one pass.
@@ -285,19 +304,16 @@ const luaL_Reg FUNCTIONS[] = {
// @param w integer
// @param h integer
// @param radius integer
// @param background GuiColor Surface behind the anti-aliased edge.
// @param top GuiColor|nil Fill, or gradient top; omitted for no fill.
// @param bottom GuiColor|nil Gradient bottom; defaults to top. Panels
// @param background ScreenColor Surface behind the anti-aliased edge.
// @param top ScreenColor|nil Fill, or gradient top; omitted for no fill.
// @param bottom ScreenColor|nil Gradient bottom; defaults to top. Panels
// without a gradient use top.
// @param border GuiColor|nil Omitted for no border.
// @param border ScreenColor|nil Omitted for no border.
{"roundRect", roundRect},
// ---Temporarily gives the app the full panel, including firmware chrome.
// @param on boolean
{"setFullscreen", setFullscreen},
// --- Fills a polygon.
// @param xs integer[]
// @param ys integer[]
// @param color GuiColor
// @param color ScreenColor
{"fillPolygon", fillPolygon},
// --- Draws a bitmap.
// @param path string Absolute BMP path.
@@ -309,31 +325,31 @@ const luaL_Reg FUNCTIONS[] = {
// @return string|nil error
{"drawBmp", drawBmp},
// --- Measures a text run.
// @param font GuiFont Use a named gui.FONT_* role.
// @param font ScreenFont Use a named screen.FONT_* role.
// @param text string
// @param style GuiTextStyle|nil Defaults to gui.STYLE_NORMAL.
// @param style ScreenTextStyle|nil Defaults to screen.STYLE_NORMAL.
// @return integer
{"getTextWidth", getTextWidth},
// --- Returns the line height of a font role.
// @param font GuiFont Use a named gui.FONT_* role.
// @param style GuiTextStyle|nil Defaults to gui.STYLE_NORMAL.
// @param font ScreenFont Use a named screen.FONT_* role.
// @param style ScreenTextStyle|nil Defaults to screen.STYLE_NORMAL.
// @return integer
{"getFontHeight", getFontHeight},
// --- Draws a text run with its top-left corner at x, y.
// @param font GuiFont Use a named gui.FONT_* role.
// @param font ScreenFont Use a named screen.FONT_* role.
// @param x integer Left edge.
// @param y integer Top edge.
// @param text string
// @param color GuiColor|nil Defaults to black.
// @param style GuiTextStyle|nil Defaults to gui.STYLE_NORMAL.
// @param background GuiColor|nil Omitted for transparent text.
// @param color ScreenColor|nil Defaults to black.
// @param style ScreenTextStyle|nil Defaults to screen.STYLE_NORMAL.
// @param background ScreenColor|nil Omitted for transparent text.
{"drawText", drawText},
{nullptr, nullptr},
};
} // namespace
void registerGui(lua_State* state) {
void registerScreen(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
const FontIds fonts = Runtime::from(state)->gui().fonts();
setField(state, "FONT_SMALL", fonts.small);
@@ -342,7 +358,7 @@ void registerGui(lua_State* state) {
setField(state, "FONT_LARGE", fonts.large);
setField(state, "STYLE_NORMAL", fonts.styleNormal);
setField(state, "STYLE_BOLD", fonts.styleBold);
lua_setglobal(state, "gui");
lua_setglobal(state, "screen");
}
} // namespace bindings
@@ -1,4 +1,4 @@
// @lua-module node NodeLib
// @lua-module tree TreeLib
// @lua-preamble ---@alias NodeId integer
// @lua-preamble ---@alias NodeType "box"|"text"|"button"|"custom"
// @lua-preamble ---@alias NodeDirection "up"|"down"|"left"|"right"
@@ -16,27 +16,27 @@
// @lua-preamble ---@field capture? boolean
// @lua-preamble ---@field interactive? boolean
// @lua-preamble ---@field label? string
// @lua-preamble ---@field font? GuiFont
// @lua-preamble ---@field font? ScreenFont
// @lua-preamble
// @lua-preamble ---@class NodeStyle
// @lua-preamble ---@field color? GuiColor
// @lua-preamble ---@field background? GuiColor Background offered to
// @lua-preamble ---@field color? ScreenColor
// @lua-preamble ---@field background? ScreenColor Background offered to
// descendants.
// @lua-preamble ---@field fill? GuiColor Surface painted by a box.
// @lua-preamble ---@field border? GuiColor
// @lua-preamble ---@field face? GuiColor Default button surface.
// @lua-preamble ---@field pressedFace? GuiColor Pressed button surface.
// @lua-preamble ---@field pressedColor? GuiColor Pressed button text.
// @lua-preamble ---@field focusColor? GuiColor Distinct outline for directional
// focus.
// @lua-preamble ---@field fill? ScreenColor Surface painted by a box.
// @lua-preamble ---@field border? ScreenColor
// @lua-preamble ---@field face? ScreenColor Default button surface.
// @lua-preamble ---@field pressedFace? ScreenColor Pressed button surface.
// @lua-preamble ---@field pressedColor? ScreenColor Pressed button text.
// @lua-preamble ---@field focusColor? ScreenColor Distinct outline for
// directional focus.
// @lua-preamble ---@field radius? integer
// @lua-preamble ---@field font? GuiFont
// @lua-preamble ---@field textStyle? GuiTextStyle
// @lua-preamble ---@field font? ScreenFont
// @lua-preamble ---@field textStyle? ScreenTextStyle
#include <cstdlib>
#include "../../node/painter.h"
#include "../helpers.h"
#include "../../../node/painter.h"
#include "../../helpers.h"
namespace esp32lua {
namespace bindings {
@@ -616,9 +616,9 @@ const luaL_Reg FUNCTIONS[] = {
} // namespace
void registerNode(lua_State* state) {
void registerTree(lua_State* state) {
luaL_newlib(state, FUNCTIONS);
lua_setglobal(state, "node");
lua_setglobal(state, "tree");
}
} // namespace bindings
+17 -22
View File
@@ -40,8 +40,21 @@ int isTouched(lua_State* state) {
return 1;
}
// @lua-augment settings SettingsLib
const luaL_Reg SETTINGS_FUNCTIONS[] = {
// @lua-module touch TouchLib
const luaL_Reg FUNCTIONS[] = {
// --- Returns the calibrated touch point, or nothing when the panel is not
// touched.
// @return integer|nil x
// @return integer|nil y
{"getPoint", getTouch},
// --- Returns the uncalibrated touch reading, or nothing when the panel is
// not touched.
// @return integer|nil x
// @return integer|nil y
{"getRawPoint", getRawTouch},
// --- Whether the panel is currently touched.
// @return boolean
{"isTouched", isTouched},
// --- Persists the panel's touch calibration.
// @param x0 integer Raw reading at the left edge.
// @param y0 integer Raw reading at the top edge.
@@ -53,29 +66,11 @@ const luaL_Reg SETTINGS_FUNCTIONS[] = {
{nullptr, nullptr},
};
// @lua-augment input InputLib
const luaL_Reg INPUT_FUNCTIONS[] = {
// --- Returns the calibrated touch point, or nothing when the panel is not
// touched.
// @return integer|nil x
// @return integer|nil y
{"getTouch", getTouch},
// --- Returns the uncalibrated touch reading, or nothing when the panel is
// not touched.
// @return integer|nil x
// @return integer|nil y
{"getRawTouch", getRawTouch},
// --- Whether the panel is currently touched.
// @return boolean
{"isTouched", isTouched},
{nullptr, nullptr},
};
} // namespace
void registerTouch(lua_State* state) {
augmentGlobal(state, "settings", SETTINGS_FUNCTIONS);
augmentGlobal(state, "input", INPUT_FUNCTIONS);
luaL_newlib(state, FUNCTIONS);
lua_setglobal(state, "touch");
}
} // namespace bindings
File diff suppressed because one or more lines are too long
+3 -5
View File
@@ -123,13 +123,11 @@ int Runtime::loadFile(lua_State* state) {
return 2;
}
void Runtime::installLoader(const std::string& appDir) {
// package.path is left to main.lua, which is loaded by absolute path and knows
// where its libraries and its apps are.
void Runtime::installLoader() {
lua_getglobal(state_, "package");
const std::string path = appDir + "/?.lua;" + paths_.lib + "/?.lua";
lua_pushlstring(state_, path.data(), path.size());
lua_setfield(state_, -2, "path");
// Keep the preload searcher, drop the C loaders: they can only report
// misleading errors about shared objects that were never there.
lua_getfield(state_, -1, "searchers");
+94 -46
View File
@@ -11,12 +11,11 @@ namespace bindings {
void registerBle(lua_State* state);
void registerButtons(lua_State* state);
void registerFs(lua_State* state);
void registerGui(lua_State* state);
void registerHttp(lua_State* state);
void registerLog(lua_State* state);
void registerNode(lua_State* state);
void registerSettings(lua_State* state);
void registerScreen(lua_State* state);
void registerSys(lua_State* state);
void registerTree(lua_State* state);
void registerTimer(lua_State* state);
void registerTouch(lua_State* state);
void registerWifi(lua_State* state);
@@ -45,17 +44,16 @@ bool isSafeRoute(const std::string& path) {
} // namespace
Runtime::Runtime(const Providers& providers, const Paths& paths)
: providers_(providers), paths_(paths) {}
Runtime::Runtime(const Providers& providers) : providers_(providers) {}
Runtime::~Runtime() { close(); }
bool Runtime::open() {
if (state_)
return true;
if (!providers_.log || !providers_.settings || !providers_.sys ||
!providers_.fs || !providers_.gui || !providers_.http ||
!providers_.timer || !providers_.wifi || !providers_.ble) {
if (!providers_.log || !providers_.sys || !providers_.fs ||
!providers_.http || !providers_.timer || !providers_.wifi ||
!providers_.ble) {
return false;
}
@@ -65,19 +63,20 @@ bool Runtime::open() {
*static_cast<Runtime**>(lua_getextraspace(state_)) = this;
luaL_openlibs(state_);
// Primary Namespaces
bindings::registerBle(state_);
bindings::registerFs(state_);
bindings::registerGui(state_);
bindings::registerHttp(state_);
bindings::registerLog(state_);
bindings::registerNode(state_);
bindings::registerSettings(state_);
bindings::registerSys(state_);
bindings::registerTimer(state_);
bindings::registerWifi(state_);
// Feature namespaces extend the tables the core registrations just created,
// so they always follow them.
// Feature Namespaces
if (providers_.gui) {
bindings::registerScreen(state_);
bindings::registerTree(state_);
}
if (providers_.touch)
bindings::registerTouch(state_);
if (providers_.buttons)
@@ -91,8 +90,7 @@ void Runtime::close() {
cancelAllTimers();
lua_close(state_);
state_ = nullptr;
// Node handles mean nothing to the next lua_State, so an app that inherited
// the previous tree would build onto its nodes.
mainRef_ = 0;
tree_.reset();
appPath_.clear();
appTitle_.clear();
@@ -103,18 +101,45 @@ Runtime::Batch::Batch(Runtime& runtime) : runtime_(runtime) {
}
Runtime::Batch::~Batch() {
if (--runtime_.batchDepth_ == 0)
if (--runtime_.batchDepth_ == 0 && runtime_.providers_.gui)
runtime_.providers_.gui->commit();
}
bool Runtime::beginCall(const char* name) {
lua_getglobal(state_, name);
if (!mainRef_)
return false;
lua_rawgeti(state_, LUA_REGISTRYINDEX, mainRef_);
lua_getfield(state_, -1, name);
lua_remove(state_, -2);
if (lua_isfunction(state_, -1))
return true;
lua_pop(state_, 1);
return false;
}
std::string Runtime::mainField(const char* key, const char* fallback) {
if (!mainRef_)
return fallback;
lua_rawgeti(state_, LUA_REGISTRYINDEX, mainRef_);
lua_getfield(state_, -1, key);
const char* value = lua_tostring(state_, -1);
const std::string result = value ? value : fallback;
lua_pop(state_, 2);
return result;
}
// Calls a chunk or handler that leaves one value on the stack; the caller owns
// it.
bool Runtime::finishCallValue(const char* name) {
if (lua_pcall(state_, 0, 1, 0) == LUA_OK)
return true;
const char* message = lua_tostring(state_, -1);
providers_.log->write(LogLevel::Error, std::string(name) + ": " +
(message ? message : "failed"));
lua_pop(state_, 1);
return false;
}
bool Runtime::finishCall(const char* name, int argc) {
if (lua_pcall(state_, argc, 0, 0) == LUA_OK)
return true;
@@ -126,17 +151,16 @@ bool Runtime::finishCall(const char* name, int argc) {
}
// @lua-global core/runtime
// @lua-preamble -- Runtime layout:
// @lua-preamble -- /.lua/apps/<AppId>/main.lua application entry
// point
// @lua-preamble -- /.lua/apps/<AppId>/<Subapp>/main.lua nested route,
// omitted from the launcher
// @lua-preamble -- /.lua/data/<AppId>/ persistent app
// data, preserved across updates
// @lua-preamble -- /.lua/lib/<module>.lua shared require()
// modules
// @lua-preamble -- require() also searches the running application's
// directory
// @lua-preamble -- The firmware loads /.lua/main.lua into every fresh state and
// calls these on the
// @lua-preamble -- table it returns. Where apps live, what surrounds them and
// which of these an app
// @lua-preamble -- itself sees are all main.lua's to decide.
// @lua-preamble --
// @lua-preamble -- Fields the firmware reads: home, the route sys.back() lands
// on once history is
// @lua-preamble -- empty, and data, the sys.getAppDataPath() template whose ?
// is the app id.
// @lua-preamble --
// @lua-preamble -- The firmware does not clear the frame before calling draw(),
// and commits changed
@@ -145,20 +169,22 @@ bool Runtime::finishCall(const char* name, int argc) {
// @lua-preamble -- Timer callbacks are registered directly with
// timer.after/every.
// ---Required. Runs once before the first draw; failing here stops the app.
// ---Required. Mounts the route; failing here leaves no app running.
// @param route string The app path sys.launch, sys.back or the boot recorded.
// @param arg string|nil The string passed to sys.launch or sys.replace.
// @lua-fn init
bool Runtime::callInit(const std::string& arg) {
// @lua-fn start
bool Runtime::callStart(const std::string& route, const std::string& arg) {
const Batch batch(*this);
if (!beginCall("init")) {
providers_.log->write(LogLevel::Error, "init: the app defines none");
if (!beginCall("start")) {
providers_.log->write(LogLevel::Error, "start: main.lua defines none");
return false;
}
lua_pushlstring(state_, route.data(), route.size());
lua_pushlstring(state_, arg.data(), arg.size());
return finishCall("init", 1);
return finishCall("start", 2);
}
// ---Optional frame loop, called once after init and then at most 30 FPS, best
// ---Optional frame loop, called once after start and then at most 30 FPS, best
// effort.
// @param deltaMs integer Monotonic milliseconds since the previous draw; zero
// on the first.
@@ -280,9 +306,19 @@ std::string Runtime::appId() const {
return slash == std::string::npos ? appPath_ : appPath_.substr(0, slash);
}
std::string Runtime::appDataPath() const { return paths_.data + "/" + appId(); }
// The template is main.lua's; substituting the app id here keeps every app on
// its own directory whatever tree the card uses.
std::string Runtime::appDataPath() const {
const size_t mark = dataTemplate_.find('?');
if (mark == std::string::npos)
return dataTemplate_;
return dataTemplate_.substr(0, mark) + appId() +
dataTemplate_.substr(mark + 1);
}
bool Runtime::hasFeature(const std::string& feature) const {
if (feature == "screen")
return providers_.gui != nullptr;
if (feature == "touch")
return providers_.touch != nullptr;
if (feature == "buttons")
@@ -302,21 +338,33 @@ bool Runtime::startApp(const std::string& path, const std::string& arg) {
appPath_ = path;
appTitle_ = appId();
const std::string directory = paths_.apps + "/" + path;
installLoader(directory);
if (!loadScript(directory + "/main.lua")) {
installLoader();
// main.lua runs first and start() mounts the route, so an app that fails
// either way leaves nothing behind.
if (!loadMain() || !callStart(path, arg)) {
close();
return false;
}
return true;
}
bool Runtime::loadMain() {
if (!loadScript(MAIN_PATH)) {
providers_.log->write(LogLevel::Error, lua_tostring(state_, -1)
? lua_tostring(state_, -1)
: "load failed");
close();
: "cannot load main.lua");
return false;
}
// The chunk body runs first, then init(), so an app that fails either way
// leaves nothing behind.
if (!finishCall("main.lua", 0) || !callInit(arg)) {
close();
if (!finishCallValue("main.lua"))
return false;
if (!lua_istable(state_, -1)) {
providers_.log->write(LogLevel::Error, "main.lua returned no table");
lua_pop(state_, 1);
return false;
}
mainRef_ = luaL_ref(state_, LUA_REGISTRYINDEX);
home_ = mainField("home", "Home");
dataTemplate_ = mainField("data", "/.lua/data/?");
return true;
}
@@ -341,7 +389,7 @@ bool Runtime::applyPendingNavigation() {
if (pending.kind == Pending::Back) {
// An empty history means the launcher, which is an app like any other.
Route target;
target.path = paths_.home;
target.path = home_;
if (!history_.empty()) {
target = history_.back();
history_.pop_back();
+16 -20
View File
@@ -23,28 +23,19 @@ struct Log : LogProvider {
}
};
struct Settings : SettingsProvider {
int32_t degrees = 0;
std::string tz = "UTC0";
int32_t rotation() const override { return degrees; }
Status setRotation(int32_t value) override {
degrees = value;
return Status::success();
}
std::string timezone() const override { return tz; }
Status setTimezone(const std::string& value) override {
tz = value;
return Status::success();
}
};
struct Sys : SysProvider {
std::string tz = "UTC0";
int32_t millis() const override { return 1234; }
MemoryInfo memory() const override {
const MemoryInfo info = {100, 200, 50};
return info;
}
bool isClockSynced() const override { return true; }
std::string timezone() const override { return tz; }
Status setTimezone(const std::string& value) override {
tz = value;
return Status::success();
}
};
struct Fs : FsProvider {
@@ -148,7 +139,7 @@ struct Fs : FsProvider {
struct Gui : GuiProvider {
std::string trace;
int32_t degrees = 0;
bool fullscreen = false;
std::string themeName = "light";
bool gradient = false;
FontIds fonts() const override {
@@ -158,7 +149,15 @@ struct Gui : GuiProvider {
int32_t width() const override { return 320; }
int32_t height() const override { return 240; }
int32_t rotation() const override { return degrees; }
void setRotation(int32_t value) override { degrees = value; }
Status setRotation(int32_t value) override {
degrees = value;
return Status::success();
}
std::string theme() const override { return themeName; }
Status setTheme(const std::string& value) override {
themeName = value;
return Status::success();
}
int32_t color(int32_t r, int32_t g, int32_t b) const override {
return (r << 16) | (g << 8) | b;
}
@@ -189,7 +188,6 @@ struct Gui : GuiProvider {
trace += border ? ",border" : ",-";
trace += ");";
}
void setFullscreen(bool on) override { fullscreen = on; }
// Stands in for an e-ink panel, where a second commit is a second visible
// refresh.
void commit() override { commits++; }
@@ -353,7 +351,6 @@ struct Buttons : ButtonsProvider {
// Every provider a Runtime needs, so a test names only what it asserts on.
struct Bench {
Log log;
Settings settings;
Sys sys;
Fs fs;
Gui gui;
@@ -367,7 +364,6 @@ struct Bench {
Providers providers() {
Providers providers;
providers.log = &log;
providers.settings = &settings;
providers.sys = &sys;
providers.fs = &fs;
providers.gui = &gui;
+141 -95
View File
@@ -37,13 +37,17 @@ int main() {
assert(bench.log.level == esp32lua::LogLevel::Info);
assert(bench.log.message == "shared runtime");
run(state, "assert(settings.getRotation() == 0)\n"
"assert(settings.setRotation(90))\n"
"assert(settings.getTimezone() == 'UTC0')\n"
"assert(settings.setTimezone('EST5EDT'))");
assert(bench.settings.degrees == 90);
assert(bench.settings.tz == "EST5EDT");
expectError(state, "settings.setRotation(45)");
run(state, "assert(sys.hasFeature('screen'))\n"
"assert(screen.getRotation() == 0)\n"
"assert(screen.setRotation(90))\n"
"assert(screen.getTheme() == 'light')\n"
"assert(screen.setTheme('dark'))\n"
"assert(sys.getTimezone() == 'UTC0')\n"
"assert(sys.setTimezone('EST5EDT'))");
assert(bench.gui.degrees == 90);
assert(bench.gui.themeName == "dark");
assert(bench.sys.tz == "EST5EDT");
expectError(state, "screen.setRotation(45)");
run(state, "assert(sys.getAPIVersion() == 1)\n"
"assert(sys.hasFeature('touch') and sys.hasFeature('buttons'))\n"
@@ -66,17 +70,17 @@ int main() {
assert(bench.fs.written.size() == 3);
expectError(state, "fs.readFile('/notes.txt', 999999)");
run(state, "assert(gui.getWidth() == 320 and gui.getHeight() == 240)\n"
"assert(gui.FONT_UI == 2 and gui.STYLE_BOLD == 1)\n"
"assert(gui.color(255, 0, 0) == 0xFF0000)\n"
"gui.setRotation(180)\n"
"gui.clear()\n"
"gui.fillPolygon({1, 2, 3}, {4, 5, 6}, 0)\n"
"gui.drawText(gui.FONT_UI, 0, 0, 'hi')");
run(state, "assert(screen.getWidth() == 320 and screen.getHeight() == 240)\n"
"assert(screen.FONT_UI == 2 and screen.STYLE_BOLD == 1)\n"
"assert(screen.color(255, 0, 0) == 0xFF0000)\n"
"assert(screen.setRotation(180))\n"
"screen.clear()\n"
"screen.fillPolygon({1, 2, 3}, {4, 5, 6}, 0)\n"
"screen.drawText(screen.FONT_UI, 0, 0, 'hi')");
assert(bench.gui.degrees == 180);
assert(bench.gui.trace == "clear;fillPolygon3;drawText(hi);");
expectError(state, "gui.fillPolygon({1, 2}, {3}, 0)");
expectError(state, "gui.color(300, 0, 0)");
expectError(state, "screen.fillPolygon({1, 2}, {3}, 0)");
expectError(state, "screen.color(300, 0, 0)");
run(state, "local response = http.get('https://example.test', {maxBytes = "
"16, headers = {Accept = 'text/plain'}})\n"
@@ -120,93 +124,62 @@ int main() {
expectError(state, "timer.after(0, function() end)");
run(state,
"assert(settings.setCalibration(100, 200, 300, 400))\n"
"local x, y = input.getTouch()\n"
"assert(touch.setCalibration(100, 200, 300, 400))\n"
"local x, y = touch.getPoint()\n"
"assert(x == 10 and y == 20)\n"
"assert(input.isTouched())\n"
"assert(input.isPressed('confirm') and not input.isPressed('back'))\n"
"assert(#input.getButtons() == 4 and input.getButtons()[3] == "
"assert(touch.isTouched())\n"
"assert(buttons.isPressed('confirm') and not buttons.isPressed('back'))\n"
"assert(#buttons.getAll() == 4 and buttons.getAll()[3] == "
"'confirm')");
assert(bench.touch.calibration[3] == 400);
// Chrome control and gradients are core: an e-ink provider flattens what it
// cannot show.
run(state, "gui.setFullscreen(true)\n"
"gui.roundRect(0, 0, 10, 10, 4, 0, 0xFF, 0x00)\n"
"gui.roundRect(0, 0, 10, 10, 4, 0, nil, nil, 0xFF)");
assert(bench.gui.fullscreen && bench.gui.gradient);
// Gradients are core: an e-ink provider flattens what it cannot show.
run(state, "screen.roundRect(0, 0, 10, 10, 4, 0, 0xFF, 0x00)\n"
"screen.roundRect(0, 0, 10, 10, 4, 0, nil, nil, 0xFF)");
assert(bench.gui.gradient);
assert(bench.gui.trace.find("roundRect(-,border);") != std::string::npos);
bench.gui.trace.clear();
run(state,
"node.reset()\n"
"local root = node.create(nil, {type = 'box', w = 'fill', h = 'fill', "
"tree.reset()\n"
"local root = tree.create(nil, {type = 'box', w = 'fill', h = 'fill', "
"pad = 4, gap = 2})\n"
"local label = node.create(root, {type = 'text', label = 'Hello'})\n"
"local button = node.create(root, {type = 'button', h = 40, interactive "
"local label = tree.create(root, {type = 'text', label = 'Hello'})\n"
"local button = tree.create(root, {type = 'button', h = 40, interactive "
"= true})\n"
"node.setStyle(root, {background = 0xFFFFFF, fill = 0xFFFFFF, color = 0, "
"tree.setStyle(root, {background = 0xFFFFFF, fill = 0xFFFFFF, color = 0, "
"face = 0xEEEEEE,\n"
" border = 0x333333, focusColor = 0xFF0000})\n"
"assert(node.layout(root, 0, 0, 320, 240))\n"
"local x, y, w, h = node.getRect(label)\n"
"assert(tree.layout(root, 0, 0, 320, 240))\n"
"local x, y, w, h = tree.getRect(label)\n"
"assert(x == 4 and y == 4 and w == 312 and h == 16)\n"
"assert(node.getLabel(label) == 'Hello')\n"
"assert(node.hit(root, 10, 40) == button)\n"
"assert(node.hit(root, 10, 200) == nil)\n"
"assert(node.focusFirst(root) == button)\n"
"assert(node.getFocus() == button)\n"
"assert(node.moveFocus(root, 'up') == button)\n"
"node.setPressed(button, true)\n"
"assert(node.isPressed(button))\n"
"assert(node.getCount() == 3 and node.getFootprint() > 0)\n"
"node.draw(root)\n"
"node.dropScratch()");
"assert(tree.getLabel(label) == 'Hello')\n"
"assert(tree.hit(root, 10, 40) == button)\n"
"assert(tree.hit(root, 10, 200) == nil)\n"
"assert(tree.focusFirst(root) == button)\n"
"assert(tree.getFocus() == button)\n"
"assert(tree.moveFocus(root, 'up') == button)\n"
"tree.setPressed(button, true)\n"
"assert(tree.isPressed(button))\n"
"assert(tree.getCount() == 3 and tree.getFootprint() > 0)\n"
"tree.draw(root)\n"
"tree.dropScratch()");
assert(bench.gui.trace.find("drawText(Hello);") != std::string::npos);
// The bordered box rounds its corners; the button fills without one.
assert(bench.gui.trace.find("roundRect(fill,border);") != std::string::npos);
assert(bench.gui.trace.find("roundRect(fill,-);") != std::string::npos);
expectError(state, "node.create(nil, {type = 'nope'})");
expectError(state, "tree.create(nil, {type = 'nope'})");
run(state,
"node.reset()\n"
"local root = node.create(nil, {type = 'custom', w = 'fill', h = "
"tree.reset()\n"
"local root = tree.create(nil, {type = 'custom', w = 'fill', h = "
"'fill'})\n"
"painted = 0\n"
"node.setPainter(function(id, x, y, w, h) painted = painted + w end)\n"
"assert(node.layout(root, 0, 0, 320, 240))\n"
"node.draw(root)\n"
"tree.setPainter(function(id, x, y, w, h) painted = painted + w end)\n"
"assert(tree.layout(root, 0, 0, 320, 240))\n"
"tree.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 +187,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.
@@ -231,26 +249,54 @@ int main() {
assert(noTouch.open());
noTouch.callTouch(esp32lua::TouchPhase::Down, 1, 1);
assert(headless.log.message == "callTouch without a touch provider");
run(noTouch.state(),
"assert(input.getTouch == nil and input.isPressed ~= nil)");
run(noTouch.state(), "assert(touch == nil and buttons.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
// A screenless firmware still runs: no gui provider, no screen/tree
// namespaces, but a callback batch still completes.
{
fake::Bench screenless;
esp32lua::Providers providers = screenless.providers();
providers.gui = nullptr;
esp32lua::Runtime noScreen(providers);
assert(noScreen.open());
assert(!noScreen.hasFeature("screen"));
run(noScreen.state(), "assert(not sys.hasFeature('screen'))\n"
"assert(screen == nil and tree == nil)\n"
"assert(sys.getTimezone() == 'UTC0')");
noScreen.callDraw(0);
}
// 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 +335,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"));