diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..a4d7f0a --- /dev/null +++ b/.clang-format @@ -0,0 +1,3 @@ +BasedOnStyle: LLVM +DerivePointerAlignment: false +PointerAlignment: Left diff --git a/.clang-format-ignore b/.clang-format-ignore new file mode 100644 index 0000000..75d1216 --- /dev/null +++ b/.clang-format-ignore @@ -0,0 +1,2 @@ +native/src/embedded_modules.cpp +native/src/vendor/** diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index 69fa449..1939aa2 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ _build/ +__pycache__ +/compile_commands.json diff --git a/Makefile b/Makefile index 8cce921..fe74958 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ 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) -.PHONY: api test embed +.PHONY: api test embed compiledb embed: $(LUAC32) @$(PYTHON) tools/embed.py $(LUAC32) lua/lib native/src/embedded_modules.cpp @@ -22,7 +22,11 @@ embed: $(LUAC32) api: @$(PYTHON) tools/gen_api.py +compiledb: + @bear -- $(MAKE) -B $(RUNTIME_TEST) $(LUA_SMOKE) $(LUAC32) + test: $(LUA_SMOKE) $(RUNTIME_TEST) $(LUAC32) + @printf '%-32s ' tools/test_gen_api.py; $(PYTHON) tools/test_gen_api.py @$(PYTHON) tools/gen_api.py --check @printf '%-32s ' generated-api; echo ok @$(PYTHON) tools/embed.py $(LUAC32) lua/lib native/src/embedded_modules.cpp --check diff --git a/flake.nix b/flake.nix index 711300c..fdc6a2b 100644 --- a/flake.nix +++ b/flake.nix @@ -23,6 +23,7 @@ { default = pkgs.mkShell { packages = with pkgs; [ + bear gnumake lua5_4 python3 diff --git a/native/include/lua/embedded_modules.h b/native/include/lua/embedded_modules.h index 6d2e07b..b479a78 100644 --- a/native/include/lua/embedded_modules.h +++ b/native/include/lua/embedded_modules.h @@ -4,8 +4,9 @@ namespace esp32lua { -// A platform module compiled to bytecode and linked into the firmware. The module searcher -// tries the SD card first, so a local copy shadows the packaged one. +// A platform module compiled to bytecode and linked into the firmware. The +// module searcher tries the SD card first, so a local copy shadows the packaged +// one. struct EmbeddedModule { const char* name; const unsigned char* data; @@ -15,4 +16,4 @@ struct EmbeddedModule { extern const EmbeddedModule embedded_modules[]; extern const size_t embedded_modules_count; -} // namespace esp32lua +} // namespace esp32lua diff --git a/native/include/lua/layout.h b/native/include/lua/layout.h index 6f9eb92..9a6c5c2 100644 --- a/native/include/lua/layout.h +++ b/native/include/lua/layout.h @@ -1,15 +1,16 @@ #pragma once -// The widget tree: structure, block-flow layout, labels and style, over a flat node -// arena. Free of Arduino headers so test/ui_layout_test.cpp can exercise it on the host. -// Ported from the Lua toolkit's Component:measure/place, whose semantics the tests -// still describe. +// The widget tree: structure, block-flow layout, labels and style, over a flat +// node arena. Free of Arduino headers so test/ui_layout_test.cpp can exercise +// it on the host. Ported from the Lua toolkit's Component:measure/place, whose +// semantics the tests still describe. // -// The tree is split across two arenas because the halves have different lifetimes. A -// Node holds what hit testing and repainting need forever: 16 bytes. A Spec holds what -// only measure() and place() read -- requested sizes, padding, gap, alignment -- and is -// dropped when the pass ends. Re-layout rebuilds from Lua rather than retaining ~20 -// bytes per node against a rotation nobody measures in milliseconds. +// The tree is split across two arenas because the halves have different +// lifetimes. A Node holds what hit testing and repainting need forever: 16 +// bytes. A Spec holds what only measure() and place() read -- requested sizes, +// padding, gap, alignment -- and is dropped when the pass ends. Re-layout +// rebuilds from Lua rather than retaining ~20 bytes per node against a rotation +// nobody measures in milliseconds. #include #include @@ -22,17 +23,17 @@ namespace ui { constexpr uint16_t NONE = 0xFFFF; -// Distinguishes "no constraint" from a real zero, which a plain int cannot: an auto-sized -// parent genuinely has no width to hand down, and a child asking for a fraction of it is -// an error rather than a zero-width silence. +// Distinguishes "no constraint" from a real zero, which a plain int cannot: an +// auto-sized parent genuinely has no width to hand down, and a child asking for +// a fraction of it is an error rather than a zero-width silence. constexpr int UNKNOWN = INT32_MIN; enum Type : uint8_t { BOX, TEXT, BUTTON, CUSTOM }; enum Flag : uint8_t { - ROW = 1 << 0, // main axis is horizontal - CAPTURE = 1 << 1, // swallows the taps its children missed - INTERACTIVE = 1 << 2, // has an on_press + ROW = 1 << 0, // main axis is horizontal + CAPTURE = 1 << 1, // swallows the taps its children missed + INTERACTIVE = 1 << 2, // has an on_press DIRTY = 1 << 3, PRESSED = 1 << 4, }; @@ -40,14 +41,15 @@ enum Flag : uint8_t { enum SizeMode : uint8_t { AUTO, PX, FRACTION, FILL }; enum Align : uint8_t { START, CENTER, END, BETWEEN }; -// Which roles a style states for itself. Anything unset is answered by the nearest -// ancestor that does state it, so styling stays in one place and a node that names no -// colours costs no bytes at all. +// Which roles a style states for itself. Anything unset is answered by the +// nearest ancestor that does state it, so styling stays in one place and a node +// that names no colours costs no bytes at all. enum StyleField : uint16_t { S_COLOR = 1 << 0, - // The background a node offers its descendants to draw text on, which is not the same - // as the fill it paints: a dialog layer hands the lit palette down while painting - // nothing itself. What is physically behind a node is derived at paint time, never set. + // The background a node offers its descendants to draw text on, which is not + // the same as the fill it paints: a dialog layer hands the lit palette down + // while painting nothing itself. What is physically behind a node is derived + // at paint time, never set. S_BG = 1 << 1, S_FILL = 1 << 2, S_BORDER = 1 << 3, @@ -76,8 +78,8 @@ struct Style { uint16_t set = 0; }; -// FRACTION is per-mille rather than a float: 0.85 of 320 is 272 either way, and the -// firmware has no business rounding differently to the host. +// FRACTION is per-mille rather than a float: 0.85 of 320 is 272 either way, and +// the firmware has no business rounding differently to the host. struct Size { SizeMode mode = AUTO; int16_t value = 0; @@ -93,8 +95,8 @@ struct Size { static Size fill() { return make(FILL, 0); } }; -// Persistent. w/h hold the measured size between measure() and place(), and the final -// rect afterwards, because the two are never needed at once. +// Persistent. w/h hold the measured size between measure() and place(), and the +// final rect afterwards, because the two are never needed at once. struct Node { int16_t x = 0, y = 0, w = 0, h = 0; uint16_t first = NONE, next = NONE, parent = NONE; @@ -107,21 +109,23 @@ struct Spec { Size w, h; Size atX, atY; bool absolute = false; - int16_t intrinsicW = 0, intrinsicH = 0; // content size of a leaf, e.g. a text run + int16_t intrinsicW = 0, + intrinsicH = 0; // content size of a leaf, e.g. a text run uint8_t padT = 0, padR = 0, padB = 0, padL = 0; uint8_t gap = 0; Align align = START; Align justify = START; - uint16_t last = NONE; // tail of the child list, so append is not a walk + uint16_t last = NONE; // tail of the child list, so append is not a walk }; -// Resistive panels land a few pixels off, so a hit box is larger than what was painted. +// Resistive panels land a few pixels off, so a hit box is larger than what was +// painted. constexpr int SLOP = 4; constexpr uint16_t NO_LABEL = 0xFFFF; class Tree { - public: +public: std::vector nodes; std::vector specs; const char* error = nullptr; @@ -137,11 +141,14 @@ class Tree { error = nullptr; } - uint16_t add(uint16_t parent, const Spec& spec, uint8_t type = BOX, uint8_t flags = 0) { - // A tree whose scratch has been dropped cannot be extended: its layout inputs are - // gone, so building again is a new screen by definition. Self-healing rather than - // advisory, because the alternative is a spec list that no longer indexes the nodes. - if (specs.size() != nodes.size()) reset(); + uint16_t add(uint16_t parent, const Spec& spec, uint8_t type = BOX, + uint8_t flags = 0) { + // A tree whose scratch has been dropped cannot be extended: its layout + // inputs are gone, so building again is a new screen by definition. + // Self-healing rather than advisory, because the alternative is a spec list + // that no longer indexes the nodes. + if (specs.size() != nodes.size()) + reset(); uint16_t id = static_cast(nodes.size()); nodes.push_back(Node()); specs.push_back(spec); @@ -149,13 +156,15 @@ class Tree { nodes[id].type = type; nodes[id].flags = flags; nodes[id].parent = parent; - if (parent != NONE) attach(parent, id); + if (parent != NONE) + attach(parent, id); return id; } - // Lua evaluates inner constructors first, so a child exists before the box that holds - // it. Adopting afterwards is what lets each spec table be garbage the moment its node - // is created, instead of a whole screen's worth of them living until the end of a build. + // Lua evaluates inner constructors first, so a child exists before the box + // that holds it. Adopting afterwards is what lets each spec table be garbage + // the moment its node is created, instead of a whole screen's worth of them + // living until the end of a build. void attach(uint16_t parent, uint16_t child) { nodes[child].parent = parent; uint16_t tail = specs[parent].last; @@ -170,42 +179,49 @@ class Tree { bool layout(uint16_t root, int x, int y, int w, int h) { error = nullptr; measure(root, w, h); - if (error) return false; + if (error) + return false; place(root, x, y, w, h); return error == nullptr; } - // Deepest interactive node wins, so a tappable child beats its tappable parent. The - // list is singly linked, so "last match walking forward" stands in for "first match - // walking backward"; they name the same node. + // Deepest interactive node wins, so a tappable child beats its tappable + // parent. The list is singly linked, so "last match walking forward" stands + // in for "first match walking backward"; they name the same node. uint16_t hit(uint16_t id, int px, int py) const { const Node& n = nodes[id]; - if (px < n.x - SLOP || px >= n.x + n.w + SLOP) return NONE; - if (py < n.y - SLOP || py >= n.y + n.h + SLOP) return NONE; + if (px < n.x - SLOP || px >= n.x + n.w + SLOP) + return NONE; + if (py < n.y - SLOP || py >= n.y + n.h + SLOP) + return NONE; uint16_t found = NONE; for (uint16_t c = n.first; c != NONE; c = nodes[c].next) { uint16_t inner = hit(c, px, py); - if (inner != NONE) found = inner; + if (inner != NONE) + found = inner; } - if (found != NONE) return found; - if (n.flags & CAPTURE) return id; + if (found != NONE) + return found; + if (n.flags & CAPTURE) + return id; return (n.flags & INTERACTIVE) ? id : NONE; } - // Layout inputs are dead once place() has run. Callers drop them here rather than - // carrying ~20 bytes a node for the lifetime of a screen. + // Layout inputs are dead once place() has run. Callers drop them here rather + // than carrying ~20 bytes a node for the lifetime of a screen. void dropScratch() { specs.clear(); specs.shrink_to_fit(); } - // Labels share one NUL-separated arena, so a text node costs two bytes plus its - // characters rather than a string object. + // Labels share one NUL-separated arena, so a text node costs two bytes plus + // its characters rather than a string object. // - // ponytail: a longer label appends and abandons the old bytes. The clock repaints - // every second at a fixed width, which overwrites in place, so the arena only grows - // when a label genuinely gets longer. Compact on rebuild if some app proves otherwise. + // ponytail: a longer label appends and abandons the old bytes. The clock + // repaints every second at a fixed width, which overwrites in place, so the + // arena only grows when a label genuinely gets longer. Compact on rebuild if + // some app proves otherwise. void setLabel(uint16_t id, const char* text) { size_t length = strlen(text); uint16_t at = labelAt[id]; @@ -223,14 +239,14 @@ class Tree { } size_t footprint() const { - return nodes.size() * sizeof(Node) + labelAt.size() * sizeof(uint16_t) + labels.size() + - styles.size() * sizeof(styles[0]); + return nodes.size() * sizeof(Node) + labelAt.size() * sizeof(uint16_t) + + labels.size() + styles.size() * sizeof(styles[0]); } - // Styles are sparse because inheritance means almost every node states nothing: a - // screen's root carries the palette and a handful of nodes override one role. Ids are - // handed out in increasing order during a build, so appends keep the list sorted and - // lookup is a binary search. + // Styles are sparse because inheritance means almost every node states + // nothing: a screen's root carries the palette and a handful of nodes + // override one role. Ids are handed out in increasing order during a build, + // so appends keep the list sorted and lookup is a binary search. Style& styleFor(uint16_t id) { size_t low = 0, high = styles.size(); while (low < high) { @@ -241,17 +257,21 @@ class Tree { high = mid; } } - if (low < styles.size() && styles[low].first == id) return styles[low].second; - return styles.insert(styles.begin() + low, std::make_pair(id, Style()))->second; + if (low < styles.size() && styles[low].first == id) + return styles[low].second; + return styles.insert(styles.begin() + low, std::make_pair(id, Style())) + ->second; } - // The nearest style at or above `id` that states `field`, or a default one if nothing - // does. Returning the whole style lets a caller read the pair a role comes in. + // The nearest style at or above `id` that states `field`, or a default one if + // nothing does. Returning the whole style lets a caller read the pair a role + // comes in. const Style& inherited(uint16_t id, uint16_t field) const { static const Style fallback; for (uint16_t n = id; n != NONE; n = nodes[n].parent) { const Style* style = styleOf(n); - if (style && (style->set & field)) return *style; + if (style && (style->set & field)) + return *style; } return fallback; } @@ -266,69 +286,78 @@ class Tree { high = mid; } } - return (low < styles.size() && styles[low].first == id) ? &styles[low].second : nullptr; + return (low < styles.size() && styles[low].first == id) + ? &styles[low].second + : nullptr; } - private: +private: std::vector labelAt; std::vector labels; - std::vector > styles; + std::vector> styles; void fail(const char* message) { - if (!error) error = message; + if (!error) + error = message; } int resolve(Size size, int span) { switch (size.mode) { - case AUTO: - return UNKNOWN; - case PX: - return size.value; - case FILL: - if (span == UNKNOWN) { - fail("fill inside an auto-sized parent"); - return 0; - } - return span; - case FRACTION: - if (span == UNKNOWN) { - fail("fraction inside an auto-sized parent"); - return 0; - } - return span * size.value / 1000; + case AUTO: + return UNKNOWN; + case PX: + return size.value; + case FILL: + if (span == UNKNOWN) { + fail("fill inside an auto-sized parent"); + return 0; + } + return span; + case FRACTION: + if (span == UNKNOWN) { + fail("fraction inside an auto-sized parent"); + return 0; + } + return span * size.value / 1000; } return UNKNOWN; } void measure(uint16_t id, int availW, int availH) { - // By value, and re-index after recursion: measuring a child can push nodes, and a - // vector that grows moves every reference taken before it. + // By value, and re-index after recursion: measuring a child can push nodes, + // and a vector that grows moves every reference taken before it. const Spec s = specs[id]; const bool row = (nodes[id].flags & ROW) != 0; int w = resolve(s.w, availW); int h = resolve(s.h, availH); - int innerW = w != UNKNOWN ? w - s.padL - s.padR - : (availW != UNKNOWN ? availW - s.padL - s.padR : UNKNOWN); - // Height is only handed down when this node was given one. A parent sized by its - // content cannot tell a child what fraction of it to take. + int innerW = w != UNKNOWN + ? w - s.padL - s.padR + : (availW != UNKNOWN ? availW - s.padL - s.padR : UNKNOWN); + // Height is only handed down when this node was given one. A parent sized + // by its content cannot tell a child what fraction of it to take. int innerH = h != UNKNOWN ? h - s.padT - s.padB : UNKNOWN; int main = 0, cross = 0, count = 0; for (uint16_t c = nodes[id].first; c != NONE; c = nodes[c].next) { measure(c, innerW, innerH); - // Absolutely placed children are measured, because they still need a size, but they - // take no part in the flow their siblings share. The Lua original counted them here - // and excluded them in place(); nothing depended on the disagreement. - if (specs[c].absolute) continue; + // Absolutely placed children are measured, because they still need a + // size, but they take no part in the flow their siblings share. The Lua + // original counted them here and excluded them in place(); nothing + // depended on the disagreement. + if (specs[c].absolute) + continue; const Node& child = nodes[c]; - if (count) main += s.gap; + if (count) + main += s.gap; if (row) { main += child.w; - if (child.h > cross) cross = child.h; + if (child.h > cross) + cross = child.h; } else { main += child.h; - if (child.w > cross) cross = child.w; + if (child.w > cross) + cross = child.w; } count++; } @@ -364,17 +393,20 @@ class Tree { int cx = x + s.padL, cy = y + s.padT; int cw = w - s.padL - s.padR, ch = h - s.padT - s.padB; - // Main-axis distribution, CSS justify-content minus the modes nothing asks for. - // Absolutely placed children take no part in the flow. + // Main-axis distribution, CSS justify-content minus the modes nothing asks + // for. Absolutely placed children take no part in the flow. int flowing = 0, used = 0; for (uint16_t c = nodes[id].first; c != NONE; c = nodes[c].next) { - if (specs[c].absolute) continue; + if (specs[c].absolute) + continue; flowing++; used += row ? nodes[c].w : nodes[c].h; } - if (flowing > 1) used += (flowing - 1) * s.gap; + if (flowing > 1) + used += (flowing - 1) * s.gap; int slack = (row ? cw : ch) - used; - if (slack < 0) slack = 0; + if (slack < 0) + slack = 0; int offset = 0, spread = 0; if (s.justify == END) { @@ -388,11 +420,14 @@ class Tree { for (uint16_t c = nodes[id].first; c != NONE; c = nodes[c].next) { const Spec cs = specs[c]; int childW = nodes[c].w, childH = nodes[c].h; - // Cross axis fills the parent unless the child asked for a size, like CSS blocks. + // Cross axis fills the parent unless the child asked for a size, like CSS + // blocks. if (row) { - if (cs.h.mode == AUTO) childH = ch; + if (cs.h.mode == AUTO) + childH = ch; } else { - if (cs.w.mode == AUTO) childW = cw; + if (cs.w.mode == AUTO) + childW = cw; } int px, py; @@ -423,5 +458,5 @@ class Tree { } }; -} // namespace ui -} // namespace esp32lua +} // namespace ui +} // namespace esp32lua diff --git a/native/include/lua/providers.h b/native/include/lua/providers.h index 07838a0..a11344b 100644 --- a/native/include/lua/providers.h +++ b/native/include/lua/providers.h @@ -7,11 +7,12 @@ namespace esp32lua { -// Every value crossing this seam is int32_t because the interpreter is built with -// LUA_32BITS: the ESP32 is a 32-bit core with no hardware float64, so a wider lua_Integer -// would cost size and speed on every value in the heap. The ceilings that follows from it -// are sys.getMillis() wrapping after ~24.9 days of uptime and file offsets stopping at -// 2 GB, neither of which a battery-powered handheld reading an SD card reaches. +// Every value crossing this seam is int32_t because the interpreter is built +// with LUA_32BITS: the ESP32 is a 32-bit core with no hardware float64, so a +// wider lua_Integer would cost size and speed on every value in the heap. The +// ceilings that follows from it are sys.getMillis() wrapping after ~24.9 days +// of uptime and file offsets stopping at 2 GB, neither of which a +// battery-powered handheld reading an SD card reaches. struct Status { bool ok; std::string error; @@ -23,7 +24,7 @@ struct Status { enum class LogLevel { Debug, Info, Error }; class LogProvider { - public: +public: virtual ~LogProvider() = default; virtual void write(LogLevel level, const std::string& message) = 0; }; @@ -35,7 +36,7 @@ struct MemoryInfo { }; class SettingsProvider { - public: +public: virtual ~SettingsProvider() = default; virtual int32_t rotation() const = 0; virtual Status setRotation(int32_t degrees) = 0; @@ -43,46 +44,57 @@ class SettingsProvider { 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. +// 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. class SysProvider { - public: +public: virtual ~SysProvider() = default; virtual int32_t millis() const = 0; virtual MemoryInfo memory() const = 0; virtual bool isClockSynced() const = 0; }; -// Scripts are streamed rather than slurped: a whole module in one buffer needs that many bytes -// contiguous, and once WiFi is up the largest free block is far smaller than the free heap. +// Scripts are streamed rather than slurped: a whole module in one buffer needs +// that many bytes contiguous, and once WiFi is up the largest free block is far +// smaller than the free heap. class FileReader { - public: +public: virtual ~FileReader() = default; // Bytes read, zero at end of file, negative on failure. virtual int32_t read(char* out, int32_t maxBytes) = 0; }; class FsProvider { - public: +public: virtual ~FsProvider() = default; - // Null when the path is missing or is a directory. The caller owns the reader. + // Null when the path is missing or is a directory. The caller owns the + // reader. virtual FileReader* openRead(const std::string& path) = 0; virtual bool exists(const std::string& path) const = 0; virtual Status fileSize(const std::string& path, int32_t& size) const = 0; - virtual Status listDirs(const std::string& path, std::vector& names) const = 0; - virtual Status listFiles(const std::string& path, std::vector& names) const = 0; + virtual Status listDirs(const std::string& path, + std::vector& names) const = 0; + virtual Status listFiles(const std::string& path, + std::vector& names) const = 0; virtual Status mkdir(const std::string& path) = 0; - virtual Status readFile(const std::string& path, int32_t maxBytes, std::string& content) const = 0; - // A line past the end reports ok with `found` false, which the binding returns as a bare nil. - virtual Status readLineAt(const std::string& path, int32_t offset, int32_t maxBytes, bool& found, - std::string& line, int32_t& nextOffset) const = 0; + virtual Status readFile(const std::string& path, int32_t maxBytes, + std::string& content) const = 0; + // A line past the end reports ok with `found` false, which the binding + // returns as a bare nil. + virtual Status readLineAt(const std::string& path, int32_t offset, + int32_t maxBytes, bool& found, std::string& line, + int32_t& nextOffset) const = 0; virtual Status remove(const std::string& path) = 0; virtual Status removeTree(const std::string& path) = 0; - virtual Status rename(const std::string& source, const std::string& destination) = 0; - virtual Status writeFile(const std::string& path, const std::string& content) = 0; + virtual Status rename(const std::string& source, + const std::string& destination) = 0; + virtual Status writeFile(const std::string& path, + const std::string& content) = 0; }; -// Native identifiers the firmware assigns to the portable font roles and text styles. +// Native identifiers the firmware assigns to the portable font roles and text +// styles. struct FontIds { int32_t small; int32_t ui; @@ -93,7 +105,7 @@ struct FontIds { }; class GuiProvider { - public: +public: virtual ~GuiProvider() = default; virtual FontIds fonts() const = 0; virtual int32_t width() const = 0; @@ -102,31 +114,44 @@ class GuiProvider { virtual void setRotation(int32_t degrees) = 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, int32_t color) = 0; - virtual void drawRect(int32_t x, int32_t y, int32_t w, int32_t h, int32_t color) = 0; - virtual void drawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, int32_t color, int32_t width) = 0; + virtual void fillRect(int32_t x, int32_t y, int32_t w, int32_t h, + int32_t color) = 0; + virtual void drawRect(int32_t x, int32_t y, int32_t w, int32_t h, + int32_t color) = 0; + virtual void drawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, + int32_t color, int32_t width) = 0; virtual void drawPixel(int32_t x, int32_t y, int32_t color) = 0; - virtual void drawCircle(int32_t x, int32_t y, int32_t radius, int32_t color, int32_t width) = 0; - virtual void fillCircle(int32_t x, int32_t y, int32_t radius, int32_t color, const int32_t* background) = 0; - // Fill, gradient, and border come from one distance field, so they cannot disagree at the - // corners. A null top paints no fill, a null border paints no outline, and a panel with no - // gradient of its own is free to ignore `bottom` the way color() quantizes to grayscale. - 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 drawCircle(int32_t x, int32_t y, int32_t radius, int32_t color, + int32_t width) = 0; + virtual void fillCircle(int32_t x, int32_t y, int32_t radius, int32_t color, + const int32_t* background) = 0; + // Fill, gradient, and border come from one distance field, so they cannot + // disagree at the corners. A null top paints no fill, a null border paints no + // outline, and a panel with no gradient of its own is free to ignore `bottom` + // the way color() quantizes to grayscale. + 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 clean up ghosting all stay here. A live LCD has nothing - // pending and does nothing. + // 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 + // clean up ghosting all stay here. A live LCD has nothing pending and does + // nothing. virtual void commit() = 0; - virtual void fillPolygon(const int32_t* xs, const int32_t* ys, size_t count, int32_t color) = 0; + virtual void fillPolygon(const int32_t* xs, const int32_t* ys, size_t count, + int32_t color) = 0; // Null coordinates centre the image; null bounds fall back to the panel size. - virtual Status drawBmp(const std::string& path, const int32_t* x, const int32_t* y, const int32_t* maxWidth, + virtual Status drawBmp(const std::string& path, const int32_t* x, + const int32_t* y, const int32_t* maxWidth, const int32_t* maxHeight) = 0; - virtual int32_t textWidth(int32_t font, const std::string& text, int32_t style) const = 0; + virtual int32_t textWidth(int32_t font, const std::string& text, + int32_t style) const = 0; virtual int32_t fontHeight(int32_t font, int32_t style) const = 0; - virtual void drawText(int32_t font, int32_t x, int32_t y, const std::string& text, int32_t color, int32_t style, + virtual void drawText(int32_t font, int32_t x, int32_t y, + const std::string& text, int32_t color, int32_t style, const int32_t* background) = 0; }; @@ -142,25 +167,29 @@ struct HttpResponse { struct HttpDownload { int32_t maxBytes; - int32_t expectedSize; // Zero when the caller did not declare one. - std::string sha256; // Empty when the caller did not declare one. + int32_t expectedSize; // Zero when the caller did not declare one. + std::string sha256; // Empty when the caller did not declare one. }; class HttpProvider { - public: +public: virtual ~HttpProvider() = default; - virtual Status request(const std::string& method, const std::string& url, const std::string& body, - const std::vector& headers, int32_t maxBytes, HttpResponse& response) = 0; - virtual Status download(const std::string& url, const std::string& destination, const HttpDownload& options, + virtual Status request(const std::string& method, const std::string& url, + const std::string& body, + const std::vector& headers, + int32_t maxBytes, HttpResponse& response) = 0; + virtual Status download(const std::string& url, + const std::string& destination, + const HttpDownload& options, int32_t& bytesWritten) = 0; }; using TimerId = int32_t; -// The firmware schedules deadlines and calls Runtime::callTimer() on the Lua thread; the -// runtime owns the identifiers and the retained callbacks. +// The firmware schedules deadlines and calls Runtime::callTimer() on the Lua +// thread; the runtime owns the identifiers and the retained callbacks. class TimerProvider { - public: +public: virtual ~TimerProvider() = default; virtual Status schedule(TimerId id, int32_t intervalMs, bool repeating) = 0; virtual void cancel(TimerId id) = 0; @@ -173,18 +202,19 @@ struct WifiNetwork { }; struct WifiStatus { - std::string state; // One of the WifiState alias values. + std::string state; // One of the WifiState alias values. std::string ssid; std::string ip; int32_t rssi; }; class WifiProvider { - public: +public: virtual ~WifiProvider() = default; virtual Status scan(std::vector& networks) = 0; // Null credentials reconnect whatever the firmware has saved. - virtual Status connect(const std::string* ssid, const std::string* password) = 0; + virtual Status connect(const std::string* ssid, + const std::string* password) = 0; virtual WifiStatus status() const = 0; virtual void disconnect() = 0; virtual Status forget() = 0; @@ -197,7 +227,7 @@ struct BleDevice { }; class BleProvider { - public: +public: virtual ~BleProvider() = default; virtual Status init(const std::string* name) = 0; virtual void deinit() = 0; @@ -205,8 +235,12 @@ class BleProvider { virtual Status connect(const std::string& address) = 0; virtual void disconnect() = 0; virtual bool isConnected() const = 0; - virtual Status read(const std::string& service, const std::string& characteristic, std::string& value) = 0; - virtual Status write(const std::string& service, const std::string& characteristic, const std::string& value) = 0; + virtual Status read(const std::string& service, + const std::string& characteristic, + std::string& value) = 0; + virtual Status write(const std::string& service, + const std::string& characteristic, + const std::string& value) = 0; virtual Status startAdvertising(const std::string* name) = 0; virtual void stopAdvertising() = 0; }; @@ -214,19 +248,21 @@ class BleProvider { enum class TouchPhase { Down, Move, Up }; class TouchProvider { - public: +public: virtual ~TouchProvider() = default; virtual bool touch(int32_t& x, int32_t& y) const = 0; virtual bool rawTouch(int32_t& x, int32_t& y) const = 0; virtual bool isTouched() const = 0; - virtual Status setCalibration(int32_t x0, int32_t y0, int32_t x1, int32_t y1) = 0; + virtual Status setCalibration(int32_t x0, int32_t y0, int32_t x1, + int32_t y1) = 0; }; class ButtonsProvider { - public: +public: virtual ~ButtonsProvider() = default; - // The roles this device maps physical buttons onto, drawn from the Button union. Hardware - // variety lives in the mapping; the vocabulary stays closed so an app stays portable. + // The roles this device maps physical buttons onto, drawn from the Button + // union. Hardware variety lives in the mapping; the vocabulary stays closed + // so an app stays portable. virtual std::vector buttons() const = 0; virtual bool isAnyPressed() const = 0; virtual bool isPressed(const std::string& button) const = 0; @@ -234,4 +270,4 @@ class ButtonsProvider { virtual bool wasReleased(const std::string& button) const = 0; }; -} // namespace esp32lua +} // namespace esp32lua diff --git a/native/include/lua/runtime.h b/native/include/lua/runtime.h index 279dd33..e19908e 100644 --- a/native/include/lua/runtime.h +++ b/native/include/lua/runtime.h @@ -11,7 +11,8 @@ struct lua_State; namespace esp32lua { -// The version sys.getAPIVersion() reports: this contract, not the firmware's build. +// The version sys.getAPIVersion() reports: this contract, not the firmware's +// build. constexpr int32_t API_VERSION = 1; // Where the runtime looks for apps, their data, and shared modules. @@ -23,8 +24,9 @@ struct Paths { std::string home = "Home"; }; -// Firmware supplies every core provider; a null feature provider is how sys.hasFeature() -// answers false, and its namespace additions are simply never registered. +// 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; @@ -41,7 +43,7 @@ struct Providers { }; class Runtime { - public: +public: explicit Runtime(const Providers& providers, const Paths& paths = Paths()); ~Runtime(); @@ -53,11 +55,14 @@ class Runtime { void close(); lua_State* state() const { return state_; } - // Replaces the running app with a fresh lua_State, loads //main.lua, and calls - // init(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()); + // Replaces the running app with a fresh lua_State, loads + // //main.lua, and calls init(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(); } - // The app-relative route, its immutable first component, and the title the app chose. + // The app-relative route, its immutable first component, and the title the + // app chose. const std::string& appPath() const { return appPath_; } std::string appId() const; std::string appDataPath() const; @@ -65,16 +70,19 @@ class Runtime { void setAppTitle(const std::string& title) { appTitle_ = title; } bool hasFeature(const std::string& feature) const; - // sys.launch/replace/back record intent and return; swapping the lua_State inside a callback - // would free the VM that is still executing. The firmware applies it between batches. - void requestLaunch(const std::string& path, const std::string& arg, bool replace); + // sys.launch/replace/back record intent and return; swapping the lua_State + // inside a callback would free the VM that is still executing. The firmware + // applies it between batches. + void requestLaunch(const std::string& path, const std::string& arg, + bool replace); void requestBack(); bool hasPendingNavigation() const { return pending_.kind != Pending::None; } - // Whether sys.back() would return somewhere rather than land on the launcher, which is what - // firmware chrome needs to decide whether to offer a back control. + // Whether sys.back() would return somewhere rather than land on the launcher, + // which is what firmware chrome needs to decide whether to offer a back + // control. bool canGoBack() const { return !history_.empty(); } - // Loads whatever was requested. False means the app failed to start or history ran out at the - // launcher, in which case no app is running. + // Loads whatever was requested. False means the app failed to start or + // history ran out at the launcher, in which case no app is running. bool applyPendingNavigation(); LogProvider& log() const { return *providers_.log; } @@ -91,9 +99,10 @@ class Runtime { 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. + // 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); void callDraw(int32_t deltaMs); // An Up phase also fires the on_touch tap alias, in that order. @@ -101,7 +110,8 @@ class Runtime { // A release also fires the on_button tap alias, in that order. void callButton(const std::string& button, bool pressed); - // Timer identity and callback retention are the runtime's; deadlines are the firmware's. + // Timer identity and callback retention are the runtime's; deadlines are the + // firmware's. TimerId addTimer(int callbackRef, int32_t intervalMs, bool repeating); bool cancelTimer(TimerId id); // Called on the Lua thread when a scheduled deadline elapses. @@ -109,7 +119,7 @@ class Runtime { static Runtime* from(lua_State* state); - private: +private: struct Timer { int callbackRef; bool repeating; @@ -131,16 +141,17 @@ class Runtime { static int searchEmbedded(lua_State* state); static int loadFile(lua_State* state); - // A batch is one visit to the app, however many callbacks it fans out into: a tap fires - // on_touch_up and then the on_touch alias, and a timer can fire inside draw. Committing per - // callback would refresh an e-ink panel twice for one visible change, so the display is - // committed when the outermost call returns. + // A batch is one visit to the app, however many callbacks it fans out into: a + // tap fires on_touch_up and then the on_touch alias, and a timer can fire + // inside draw. Committing per callback would refresh an e-ink panel twice for + // one visible change, so the display is committed when the outermost call + // returns. class Batch { - public: + public: explicit Batch(Runtime& runtime); ~Batch(); - private: + private: Runtime& runtime_; }; @@ -163,4 +174,4 @@ class Runtime { Pending pending_; }; -} // namespace esp32lua +} // namespace esp32lua diff --git a/native/src/bindings/core/ble.cpp b/native/src/bindings/core/ble.cpp index 60241a1..ad85055 100644 --- a/native/src/bindings/core/ble.cpp +++ b/native/src/bindings/core/ble.cpp @@ -13,7 +13,8 @@ namespace { int init(lua_State* state) { std::string name; const bool hasName = optionalString(state, 1, name); - return pushStatus(state, Runtime::from(state)->ble().init(hasName ? &name : nullptr)); + return pushStatus( + state, Runtime::from(state)->ble().init(hasName ? &name : nullptr)); } int deinit(lua_State* state) { @@ -27,7 +28,8 @@ int scan(lua_State* state) { std::vector devices; const Status status = Runtime::from(state)->ble().scan(durationMs, devices); - if (!status.ok) return pushError(state, status.error); + if (!status.ok) + return pushError(state, status.error); lua_createtable(state, static_cast(devices.size()), 0); for (size_t at = 0; at < devices.size(); at++) { @@ -59,8 +61,10 @@ int read(lua_State* state) { const std::string service = checkString(state, 1); const std::string characteristic = checkString(state, 2); std::string value; - const Status status = Runtime::from(state)->ble().read(service, characteristic, value); - if (!status.ok) return pushError(state, status.error); + const Status status = + Runtime::from(state)->ble().read(service, characteristic, value); + if (!status.ok) + return pushError(state, status.error); pushString(state, value); return 1; } @@ -69,13 +73,15 @@ int write(lua_State* state) { const std::string service = checkString(state, 1); const std::string characteristic = checkString(state, 2); const std::string value = checkString(state, 3); - return pushStatus(state, Runtime::from(state)->ble().write(service, characteristic, value)); + return pushStatus( + state, Runtime::from(state)->ble().write(service, characteristic, value)); } int startAdvertising(lua_State* state) { std::string name; const bool hasName = optionalString(state, 1, name); - return pushStatus(state, Runtime::from(state)->ble().startAdvertising(hasName ? &name : nullptr)); + return pushStatus(state, Runtime::from(state)->ble().startAdvertising( + hasName ? &name : nullptr)); } int stopAdvertising(lua_State* state) { @@ -129,12 +135,12 @@ const luaL_Reg FUNCTIONS[] = { {nullptr, nullptr}, }; -} // namespace +} // namespace void registerBle(lua_State* state) { luaL_newlib(state, FUNCTIONS); lua_setglobal(state, "ble"); } -} // namespace bindings -} // namespace esp32lua +} // namespace bindings +} // namespace esp32lua diff --git a/native/src/bindings/core/fs.cpp b/native/src/bindings/core/fs.cpp index 6082415..c1a6981 100644 --- a/native/src/bindings/core/fs.cpp +++ b/native/src/bindings/core/fs.cpp @@ -1,5 +1,6 @@ // @lua-module fs FsLib -// @lua-const MAX_READ_BYTES integer 65536 Largest portable whole-file or line read. +// @lua-const MAX_READ_BYTES integer 65536 Largest portable whole-file or line +// read. #include "../helpers.h" @@ -11,7 +12,8 @@ constexpr int32_t MAX_READ_BYTES = 65536; int32_t checkReadLimit(lua_State* state, int index) { const int32_t maxBytes = checkInt(state, index); - luaL_argcheck(state, maxBytes >= 0 && maxBytes <= MAX_READ_BYTES, index, "exceeds fs.MAX_READ_BYTES"); + luaL_argcheck(state, maxBytes >= 0 && maxBytes <= MAX_READ_BYTES, index, + "exceeds fs.MAX_READ_BYTES"); return maxBytes; } @@ -25,7 +27,8 @@ int fileSize(lua_State* state) { const std::string path = checkString(state, 1); int32_t size = 0; const Status status = Runtime::from(state)->fs().fileSize(path, size); - if (!status.ok) return pushError(state, status.error); + if (!status.ok) + return pushError(state, status.error); lua_pushinteger(state, size); return 1; } @@ -34,8 +37,10 @@ int list(lua_State* state, bool directories) { const std::string path = checkString(state, 1); std::vector names; const FsProvider& fs = Runtime::from(state)->fs(); - const Status status = directories ? fs.listDirs(path, names) : fs.listFiles(path, names); - if (!status.ok) return pushError(state, status.error); + const Status status = + directories ? fs.listDirs(path, names) : fs.listFiles(path, names); + if (!status.ok) + return pushError(state, status.error); pushStrings(state, names); return 1; } @@ -52,8 +57,10 @@ int readFile(lua_State* state) { const int32_t maxBytes = checkReadLimit(state, 2); const std::string path = checkString(state, 1); std::string content; - const Status status = Runtime::from(state)->fs().readFile(path, maxBytes, content); - if (!status.ok) return pushError(state, status.error); + const Status status = + Runtime::from(state)->fs().readFile(path, maxBytes, content); + if (!status.ok) + return pushError(state, status.error); pushString(state, content); return 1; } @@ -67,7 +74,8 @@ int readLineAt(lua_State* state) { bool found = false; int32_t nextOffset = 0; std::string line; - const Status status = Runtime::from(state)->fs().readLineAt(path, offset, maxBytes, found, line, nextOffset); + const Status status = Runtime::from(state)->fs().readLineAt( + path, offset, maxBytes, found, line, nextOffset); if (!status.ok) { lua_pushnil(state); lua_pushnil(state); @@ -96,7 +104,8 @@ int removeTree(lua_State* state) { int rename(lua_State* state) { const std::string source = checkString(state, 1); const std::string destination = checkString(state, 2); - return pushStatus(state, Runtime::from(state)->fs().rename(source, destination)); + return pushStatus(state, + Runtime::from(state)->fs().rename(source, destination)); } int writeFile(lua_State* state) { @@ -107,7 +116,8 @@ int writeFile(lua_State* state) { const luaL_Reg FUNCTIONS[] = { // --- Whether a path exists. - // @param path string Absolute SD-card path; traversal components are rejected. + // @param path string Absolute SD-card path; traversal components are + // rejected. // @return boolean {"exists", exists}, // --- Returns the size of a file in bytes. @@ -132,35 +142,43 @@ const luaL_Reg FUNCTIONS[] = { {"mkdir", mkdir}, // --- Reads a whole file. // @param path string Absolute file path. - // @param maxBytes integer Maximum bytes to allocate, up to fs.MAX_READ_BYTES; oversized files fail rather than truncate. + // @param maxBytes integer Maximum bytes to allocate, up to + // fs.MAX_READ_BYTES; oversized files fail rather than truncate. // @return string|nil content // @return string|nil error {"readFile", readFile}, // --- Reads one line starting at a byte offset. // @param path string Absolute file path. - // @param offset integer Zero-based byte offset; a mid-line offset advances to the next line. - // @param maxBytes integer Maximum line bytes to allocate, up to fs.MAX_READ_BYTES. + // @param offset integer Zero-based byte offset; a mid-line offset advances + // to the next line. + // @param maxBytes integer Maximum line bytes to allocate, up to + // fs.MAX_READ_BYTES. // @return string|nil line Nil at end of file or on failure. // @return integer|nil nextOffset Byte offset of the following line. - // @return string|nil error Present when the file cannot be read or the line exceeds maxBytes. + // @return string|nil error Present when the file cannot be read or the line + // exceeds maxBytes. {"readLineAt", readLineAt}, // --- Removes a file. - // @param path string Absolute file path. Firmware-protected roots cannot be removed. + // @param path string Absolute file path. Firmware-protected roots cannot be + // removed. // @return true|nil ok // @return string|nil error {"remove", remove}, // --- Removes a directory and everything below it. - // @param path string Absolute directory path. Firmware-protected roots cannot be removed. + // @param path string Absolute directory path. Firmware-protected roots + // cannot be removed. // @return true|nil ok // @return string|nil error {"removeTree", removeTree}, // --- Renames a file or directory. // @param source string Absolute source path. - // @param destination string Absolute destination path, which must not exist. + // @param destination string Absolute destination path, which must not + // exist. // @return true|nil ok // @return string|nil error {"rename", rename}, - // --- Atomically replaces the destination or leaves its previous contents intact. + // --- Atomically replaces the destination or leaves its previous contents + // intact. // @param path string Absolute file path. // @param content string // @return true|nil ok @@ -169,7 +187,7 @@ const luaL_Reg FUNCTIONS[] = { {nullptr, nullptr}, }; -} // namespace +} // namespace void registerFs(lua_State* state) { luaL_newlib(state, FUNCTIONS); @@ -178,5 +196,5 @@ void registerFs(lua_State* state) { lua_setglobal(state, "fs"); } -} // namespace bindings -} // namespace esp32lua +} // namespace bindings +} // namespace esp32lua diff --git a/native/src/bindings/core/gui.cpp b/native/src/bindings/core/gui.cpp index 35c4eb9..091fa4d 100644 --- a/native/src/bindings/core/gui.cpp +++ b/native/src/bindings/core/gui.cpp @@ -34,7 +34,8 @@ int getRotation(lua_State* state) { 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"); + luaL_argcheck(state, degrees >= 0 && degrees <= 270 && degrees % 90 == 0, 1, + "expected 0, 90, 180, or 270"); provider(state).setRotation(degrees); return 0; } @@ -59,30 +60,35 @@ int clear(lua_State* state) { } int fillRect(lua_State* state) { - provider(state).fillRect(checkInt(state, 1), checkInt(state, 2), checkInt(state, 3), checkInt(state, 4), + provider(state).fillRect(checkInt(state, 1), checkInt(state, 2), + checkInt(state, 3), checkInt(state, 4), checkInt(state, 5)); return 0; } int drawRect(lua_State* state) { - provider(state).drawRect(checkInt(state, 1), checkInt(state, 2), checkInt(state, 3), checkInt(state, 4), + provider(state).drawRect(checkInt(state, 1), checkInt(state, 2), + checkInt(state, 3), checkInt(state, 4), checkInt(state, 5)); return 0; } int drawLine(lua_State* state) { - provider(state).drawLine(checkInt(state, 1), checkInt(state, 2), checkInt(state, 3), checkInt(state, 4), + provider(state).drawLine(checkInt(state, 1), checkInt(state, 2), + checkInt(state, 3), checkInt(state, 4), checkInt(state, 5), optionalInt(state, 6, 1)); return 0; } int drawPixel(lua_State* state) { - provider(state).drawPixel(checkInt(state, 1), checkInt(state, 2), checkInt(state, 3)); + provider(state).drawPixel(checkInt(state, 1), checkInt(state, 2), + checkInt(state, 3)); return 0; } int drawCircle(lua_State* state) { - provider(state).drawCircle(checkInt(state, 1), checkInt(state, 2), checkInt(state, 3), checkInt(state, 4), + provider(state).drawCircle(checkInt(state, 1), checkInt(state, 2), + checkInt(state, 3), checkInt(state, 4), optionalInt(state, 5, 1)); return 0; } @@ -94,7 +100,8 @@ int fillCircle(lua_State* state) { const int32_t fill = checkInt(state, 4); int32_t background = 0; const bool hasBackground = optionalColor(state, 5, background); - provider(state).fillCircle(x, y, radius, fill, hasBackground ? &background : nullptr); + provider(state).fillCircle(x, y, radius, fill, + hasBackground ? &background : nullptr); return 0; } @@ -111,8 +118,10 @@ int roundRect(lua_State* state) { const bool hasTop = optionalColor(state, 7, top); const bool hasBottom = optionalColor(state, 8, bottom); const bool hasBorder = optionalColor(state, 9, border); - provider(state).roundRect(x, y, w, h, radius, background, hasTop ? &top : nullptr, - hasBottom ? &bottom : (hasTop ? &top : nullptr), hasBorder ? &border : nullptr); + provider(state).roundRect(x, y, w, h, radius, background, + hasTop ? &top : nullptr, + hasBottom ? &bottom : (hasTop ? &top : nullptr), + hasBorder ? &border : nullptr); return 0; } @@ -141,7 +150,8 @@ int fillPolygon(lua_State* state) { luaL_checktype(state, 2, LUA_TTABLE); const int32_t fill = checkInt(state, 3); - // The vectors are destroyed before any Lua error is raised, because luaL_error longjmps. + // The vectors are destroyed before any Lua error is raised, because + // luaL_error longjmps. bool usable = false; { std::vector xs; @@ -149,26 +159,31 @@ int fillPolygon(lua_State* state) { readIntegers(state, 1, xs); readIntegers(state, 2, ys); usable = !xs.empty() && xs.size() == ys.size(); - if (usable) provider(state).fillPolygon(xs.data(), ys.data(), xs.size(), fill); + if (usable) + provider(state).fillPolygon(xs.data(), ys.data(), xs.size(), fill); } - if (!usable) return luaL_error(state, "expected matching non-empty integer arrays"); + if (!usable) + return luaL_error(state, "expected matching non-empty integer arrays"); return 0; } int drawBmp(lua_State* state) { int32_t values[4] = {0, 0, 0, 0}; bool present[4] = {false, false, false, false}; - for (int at = 0; at < 4; at++) present[at] = optionalColor(state, at + 2, values[at]); + for (int at = 0; at < 4; at++) + present[at] = optionalColor(state, at + 2, values[at]); const std::string path = checkString(state, 1); - const Status status = provider(state).drawBmp(path, present[0] ? &values[0] : nullptr, - present[1] ? &values[1] : nullptr, present[2] ? &values[2] : nullptr, - present[3] ? &values[3] : nullptr); + const Status status = provider(state).drawBmp( + path, present[0] ? &values[0] : nullptr, + present[1] ? &values[1] : nullptr, present[2] ? &values[2] : nullptr, + present[3] ? &values[3] : nullptr); return pushStatus(state, status); } int getTextWidth(lua_State* state) { const int32_t font = checkInt(state, 1); - const int32_t style = optionalInt(state, 3, provider(state).fonts().styleNormal); + const int32_t style = + optionalInt(state, 3, provider(state).fonts().styleNormal); const std::string text = checkString(state, 2); lua_pushinteger(state, provider(state).textWidth(font, text, style)); return 1; @@ -176,7 +191,10 @@ int getTextWidth(lua_State* state) { int getFontHeight(lua_State* state) { const int32_t font = checkInt(state, 1); - lua_pushinteger(state, provider(state).fontHeight(font, optionalInt(state, 2, provider(state).fonts().styleNormal))); + lua_pushinteger( + state, + provider(state).fontHeight( + font, optionalInt(state, 2, provider(state).fonts().styleNormal))); return 1; } @@ -191,7 +209,8 @@ int drawText(lua_State* state) { int32_t background = 0; const bool hasBackground = optionalColor(state, 7, background); const std::string text = checkString(state, 4); - gui.drawText(font, x, y, text, textColor, style, hasBackground ? &background : nullptr); + gui.drawText(font, x, y, text, textColor, style, + hasBackground ? &background : nullptr); return 0; } @@ -208,7 +227,8 @@ const luaL_Reg FUNCTIONS[] = { // --- Returns the rotation of the live frame. // @return integer Degrees clockwise for the live frame. {"getRotation", getRotation}, - // --- Returns an opaque native color. E-ink implementations quantize RGB to available grayscale. + // --- 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. @@ -258,7 +278,8 @@ const luaL_Reg FUNCTIONS[] = { // @param color GuiColor // @param background GuiColor|nil Surface behind an anti-aliased edge. {"fillCircle", fillCircle}, - // ---Draws an anti-aliased rounded fill, optional gradient, and optional border in one pass. + // ---Draws an anti-aliased rounded fill, optional gradient, and optional + // border in one pass. // @param x integer // @param y integer // @param w integer @@ -266,7 +287,8 @@ const luaL_Reg FUNCTIONS[] = { // @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 without a gradient use top. + // @param bottom GuiColor|nil Gradient bottom; defaults to top. Panels + // without a gradient use top. // @param border GuiColor|nil Omitted for no border. {"roundRect", roundRect}, // ---Temporarily gives the app the full panel, including firmware chrome. @@ -309,7 +331,7 @@ const luaL_Reg FUNCTIONS[] = { {nullptr, nullptr}, }; -} // namespace +} // namespace void registerGui(lua_State* state) { luaL_newlib(state, FUNCTIONS); @@ -323,5 +345,5 @@ void registerGui(lua_State* state) { lua_setglobal(state, "gui"); } -} // namespace bindings -} // namespace esp32lua +} // namespace bindings +} // namespace esp32lua diff --git a/native/src/bindings/core/http.cpp b/native/src/bindings/core/http.cpp index 486f2fc..a54cff6 100644 --- a/native/src/bindings/core/http.cpp +++ b/native/src/bindings/core/http.cpp @@ -1,17 +1,22 @@ // @lua-module http HttpLib // @lua-preamble ---@class HttpRequestOptions -// @lua-preamble ---@field maxBytes integer Maximum response-body bytes to allocate, up to http.MAX_RESPONSE_BYTES; zero is valid for HEAD. +// @lua-preamble ---@field maxBytes integer Maximum response-body bytes to +// allocate, up to http.MAX_RESPONSE_BYTES; zero is valid for HEAD. // @lua-preamble ---@field headers? table // @lua-preamble // @lua-preamble ---@class HttpResponse // @lua-preamble ---@field status integer HTTP status code. -// @lua-preamble ---@field body string Response body, including for non-2xx responses. +// @lua-preamble ---@field body string Response body, including for non-2xx +// responses. // @lua-preamble // @lua-preamble ---@class HttpDownloadOptions -// @lua-preamble ---@field maxBytes integer Required maximum, from 1 through 16777216. +// @lua-preamble ---@field maxBytes integer Required maximum, from 1 through +// 16777216. // @lua-preamble ---@field expectedSize? integer Exact expected byte count. -// @lua-preamble ---@field sha256? string Exact expected SHA-256 as 64 hexadecimal characters. -// @lua-const MAX_RESPONSE_BYTES integer 65536 Largest portable in-memory response body. +// @lua-preamble ---@field sha256? string Exact expected SHA-256 as 64 +// hexadecimal characters. +// @lua-const MAX_RESPONSE_BYTES integer 65536 Largest portable in-memory +// response body. #include "../helpers.h" @@ -31,12 +36,14 @@ int32_t checkResponseLimit(lua_State* state, int index) { return maxBytes; } -void readHeaders(lua_State* state, int index, std::vector& headers) { +void readHeaders(lua_State* state, int index, + std::vector& headers) { lua_getfield(state, index, "headers"); if (lua_istable(state, -1)) { lua_pushnil(state); while (lua_next(state, -2)) { - if (lua_type(state, -2) == LUA_TSTRING && lua_type(state, -1) == LUA_TSTRING) { + if (lua_type(state, -2) == LUA_TSTRING && + lua_type(state, -1) == LUA_TSTRING) { HttpHeader header; header.name = lua_tostring(state, -2); header.value = lua_tostring(state, -1); @@ -59,8 +66,10 @@ int request(lua_State* state, const char* method, bool withBody) { readHeaders(state, optionsIndex, headers); HttpResponse response; - const Status status = Runtime::from(state)->http().request(method, url, body, headers, maxBytes, response); - if (!status.ok) return pushError(state, status.error); + const Status status = Runtime::from(state)->http().request( + method, url, body, headers, maxBytes, response); + if (!status.ok) + return pushError(state, status.error); lua_createtable(state, 0, 2); setField(state, "status", response.status); @@ -79,14 +88,18 @@ int download(lua_State* state) { lua_getfield(state, 3, "maxBytes"); const int32_t maxBytes = static_cast(luaL_checkinteger(state, -1)); lua_pop(state, 1); - luaL_argcheck(state, maxBytes >= 1 && maxBytes <= MAX_DOWNLOAD_BYTES, 3, "maxBytes must be 1 through 16777216"); + luaL_argcheck(state, maxBytes >= 1 && maxBytes <= MAX_DOWNLOAD_BYTES, 3, + "maxBytes must be 1 through 16777216"); lua_getfield(state, 3, "expectedSize"); - const int32_t expectedSize = static_cast(luaL_optinteger(state, -1, 0)); + const int32_t expectedSize = + static_cast(luaL_optinteger(state, -1, 0)); lua_pop(state, 1); lua_getfield(state, 3, "sha256"); - const char* sha256 = lua_isnil(state, -1) ? nullptr : luaL_checkstring(state, -1); - luaL_argcheck(state, !sha256 || lua_rawlen(state, -1) == 64, 3, "sha256 must be 64 hexadecimal characters"); + const char* sha256 = + lua_isnil(state, -1) ? nullptr : luaL_checkstring(state, -1); + luaL_argcheck(state, !sha256 || lua_rawlen(state, -1) == 64, 3, + "sha256 must be 64 hexadecimal characters"); HttpDownload options; options.maxBytes = maxBytes; @@ -97,8 +110,10 @@ int download(lua_State* state) { const std::string url = checkString(state, 1); const std::string destination = checkString(state, 2); int32_t bytesWritten = 0; - const Status status = Runtime::from(state)->http().download(url, destination, options, bytesWritten); - if (!status.ok) return pushError(state, status.error); + const Status status = Runtime::from(state)->http().download( + url, destination, options, bytesWritten); + if (!status.ok) + return pushError(state, status.error); lua_pushinteger(state, bytesWritten); return 1; } @@ -112,8 +127,9 @@ int urlencode(lua_State* state) { luaL_buffinit(state, &buffer); for (size_t at = 0; at < length; at++) { const unsigned char c = static_cast(input[at]); - const bool unreserved = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || - c == '_' || c == '.' || c == '~'; + const bool unreserved = (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '-' || c == '_' || + c == '.' || c == '~'; if (unreserved) { luaL_addchar(&buffer, static_cast(c)); } else { @@ -131,7 +147,8 @@ const luaL_Reg FUNCTIONS[] = { // @param url string // @param options HttpRequestOptions // @return HttpResponse|nil response - // @return string|nil error Transport failure or response body exceeding maxBytes. + // @return string|nil error Transport failure or response body exceeding + // maxBytes. {"get", get}, // --- Performs a HEAD request. // @param url string @@ -159,7 +176,8 @@ const luaL_Reg FUNCTIONS[] = { // @return HttpResponse|nil response // @return string|nil error {"patch", patch}, - // --- Streams authenticated HTTPS to a new file and removes partial or unverified output. + // --- Streams authenticated HTTPS to a new file and removes partial or + // unverified output. // @param url string HTTPS URL. // @param destination string Absolute path which must not exist. // @param options HttpDownloadOptions @@ -173,7 +191,7 @@ const luaL_Reg FUNCTIONS[] = { {nullptr, nullptr}, }; -} // namespace +} // namespace void registerHttp(lua_State* state) { luaL_newlib(state, FUNCTIONS); @@ -182,5 +200,5 @@ void registerHttp(lua_State* state) { lua_setglobal(state, "http"); } -} // namespace bindings -} // namespace esp32lua +} // namespace bindings +} // namespace esp32lua diff --git a/native/src/bindings/core/log.cpp b/native/src/bindings/core/log.cpp index 11205af..a59059e 100644 --- a/native/src/bindings/core/log.cpp +++ b/native/src/bindings/core/log.cpp @@ -33,12 +33,12 @@ const luaL_Reg FUNCTIONS[] = { {nullptr, nullptr}, }; -} // namespace +} // namespace void registerLog(lua_State* state) { luaL_newlib(state, FUNCTIONS); lua_setglobal(state, "log"); } -} // namespace bindings -} // namespace esp32lua +} // namespace bindings +} // namespace esp32lua diff --git a/native/src/bindings/core/node.cpp b/native/src/bindings/core/node.cpp index 253b166..de649d6 100644 --- a/native/src/bindings/core/node.cpp +++ b/native/src/bindings/core/node.cpp @@ -20,13 +20,15 @@ // @lua-preamble // @lua-preamble ---@class NodeStyle // @lua-preamble ---@field color? GuiColor -// @lua-preamble ---@field background? GuiColor Background offered to descendants. +// @lua-preamble ---@field background? GuiColor 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 focusColor? GuiColor Distinct outline for directional +// focus. // @lua-preamble ---@field radius? integer // @lua-preamble ---@field font? GuiFont // @lua-preamble ---@field textStyle? GuiTextStyle @@ -46,26 +48,32 @@ ui::Tree& tree(lua_State* state) { return Runtime::from(state)->tree(); } uint16_t checkNode(lua_State* state, int index) { const lua_Integer id = luaL_checkinteger(state, index); - luaL_argcheck(state, id >= 0 && static_cast(id) < tree(state).nodes.size(), index, "unknown node"); + luaL_argcheck(state, + id >= 0 && static_cast(id) < tree(state).nodes.size(), + index, "unknown node"); return static_cast(id); } -// A number below one is a fraction of the parent, kept as per-mille so the firmware and -// the host round a layout identically. +// A number below one is a fraction of the parent, kept as per-mille so the +// firmware and the host round a layout identically. ui::Size sizeAt(lua_State* state, int index, bool& present) { present = !lua_isnoneornil(state, index); ui::Size size; - if (!present) return size; + if (!present) + return size; if (lua_type(state, index) == LUA_TSTRING) { const char* mode = lua_tostring(state, index); - if (strcmp(mode, "fill") == 0) return ui::Size::fill(); - if (strcmp(mode, "auto") == 0) return size; + if (strcmp(mode, "fill") == 0) + return ui::Size::fill(); + if (strcmp(mode, "auto") == 0) + return size; luaL_error(state, "size must be a number, 'fill', or 'auto'"); return size; } const lua_Number value = luaL_checknumber(state, index); - return value > 0.0 && value < 1.0 ? ui::Size::fraction(static_cast(value * 1000.0)) - : ui::Size::px(static_cast(value)); + return value > 0.0 && value < 1.0 + ? ui::Size::fraction(static_cast(value * 1000.0)) + : ui::Size::px(static_cast(value)); } ui::Size readSize(lua_State* state, int index, const char* key, bool& present) { @@ -75,9 +83,12 @@ ui::Size readSize(lua_State* state, int index, const char* key, bool& present) { return size; } -int16_t readNumber(lua_State* state, int index, const char* key, int16_t fallback) { +int16_t readNumber(lua_State* state, int index, const char* key, + int16_t fallback) { lua_getfield(state, index, key); - const int16_t value = lua_isnoneornil(state, -1) ? fallback : static_cast(luaL_checknumber(state, -1)); + const int16_t value = lua_isnoneornil(state, -1) + ? fallback + : static_cast(luaL_checknumber(state, -1)); lua_pop(state, 1); return value; } @@ -100,8 +111,8 @@ ui::Align readAlign(lua_State* state, int index, const char* key) { align = ui::END; } else if (strcmp(value, "between") == 0) { align = ui::BETWEEN; - // "stretch" is the default cross-axis behaviour: a child without its own size - // already fills the line. + // "stretch" is the default cross-axis behaviour: a child without its own + // size already fills the line. } else if (strcmp(value, "start") != 0 && strcmp(value, "stretch") != 0) { lua_pop(state, 1); luaL_error(state, "unknown alignment '%s'", value); @@ -160,17 +171,22 @@ int create(lua_State* state) { lua_pop(state, 1); uint8_t flags = 0; - if (readFlag(state, 2, "row")) flags |= ui::ROW; - if (readFlag(state, 2, "capture")) flags |= ui::CAPTURE; - if (readFlag(state, 2, "interactive")) flags |= ui::INTERACTIVE; + if (readFlag(state, 2, "row")) + flags |= ui::ROW; + if (readFlag(state, 2, "capture")) + flags |= ui::CAPTURE; + if (readFlag(state, 2, "interactive")) + flags |= ui::INTERACTIVE; lua_getfield(state, 2, "font"); - const int32_t font = lua_isnoneornil(state, -1) ? Runtime::from(state)->gui().fonts().ui - : static_cast(luaL_checkinteger(state, -1)); + const int32_t font = lua_isnoneornil(state, -1) + ? Runtime::from(state)->gui().fonts().ui + : static_cast(luaL_checkinteger(state, -1)); lua_pop(state, 1); lua_getfield(state, 2, "label"); - const char* label = lua_isnoneornil(state, -1) ? nullptr : luaL_checkstring(state, -1); + const char* label = + lua_isnoneornil(state, -1) ? nullptr : luaL_checkstring(state, -1); if (label && type == ui::TEXT) { GuiProvider& gui = Runtime::from(state)->gui(); const int32_t style = gui.fonts().styleNormal; @@ -179,7 +195,8 @@ int create(lua_State* state) { } const uint16_t id = tree(state).add(parent, spec, type, flags); - if (label) tree(state).setLabel(id, label); + if (label) + tree(state).setLabel(id, label); lua_pop(state, 1); lua_pushinteger(state, id); return 1; @@ -188,22 +205,26 @@ int create(lua_State* state) { int attach(lua_State* state) { const uint16_t parent = checkNode(state, 1); const uint16_t child = checkNode(state, 2); - luaL_argcheck(state, tree(state).nodes[child].parent == ui::NONE, 2, "already attached"); + luaL_argcheck(state, tree(state).nodes[child].parent == ui::NONE, 2, + "already attached"); tree(state).attach(parent, child); return 0; } int setSize(lua_State* state) { const uint16_t id = checkNode(state, 1); - luaL_argcheck(state, id < tree(state).specs.size(), 1, "layout scratch has been dropped"); + luaL_argcheck(state, id < tree(state).specs.size(), 1, + "layout scratch has been dropped"); bool hasW = false; bool hasH = false; const ui::Size w = sizeAt(state, 2, hasW); const ui::Size h = sizeAt(state, 3, hasH); ui::Spec& spec = tree(state).specs[id]; - if (hasW) spec.w = w; - if (hasH) spec.h = h; + if (hasW) + spec.w = w; + if (hasH) + spec.h = h; return 0; } @@ -214,7 +235,8 @@ int layout(lua_State* state) { const int32_t w = checkInt(state, 4); const int32_t h = checkInt(state, 5); ui::Tree& nodes = tree(state); - luaL_argcheck(state, root < nodes.specs.size(), 1, "layout scratch has been dropped"); + luaL_argcheck(state, root < nodes.specs.size(), 1, + "layout scratch has been dropped"); if (nodes.layout(root, x, y, w, h)) { lua_pushboolean(state, true); return 1; @@ -231,8 +253,10 @@ int dropScratch(lua_State* state) { int hit(lua_State* state) { const uint16_t root = checkNode(state, 1); - const uint16_t found = tree(state).hit(root, checkInt(state, 2), checkInt(state, 3)); - if (found == ui::NONE) return 0; + const uint16_t found = + tree(state).hit(root, checkInt(state, 2), checkInt(state, 3)); + if (found == ui::NONE) + return 0; lua_pushinteger(state, found); return 1; } @@ -256,19 +280,22 @@ int setLabel(lua_State* state) { int getLabel(lua_State* state) { const char* label = tree(state).label(checkNode(state, 1)); - if (!label) return 0; + if (!label) + return 0; lua_pushstring(state, label); return 1; } int getParent(lua_State* state) { const uint16_t parent = tree(state).nodes[checkNode(state, 1)].parent; - if (parent == ui::NONE) return 0; + if (parent == ui::NONE) + return 0; lua_pushinteger(state, parent); return 1; } -void readStyleColor(lua_State* state, int index, const char* key, int32_t& field, uint16_t flag, uint16_t& set) { +void readStyleColor(lua_State* state, int index, const char* key, + int32_t& field, uint16_t flag, uint16_t& set) { lua_getfield(state, index, key); if (!lua_isnoneornil(state, -1)) { field = static_cast(luaL_checkinteger(state, -1)); @@ -287,11 +314,15 @@ int setStyle(lua_State* state) { readStyleColor(state, 2, "fill", style.fill, ui::S_FILL, style.set); readStyleColor(state, 2, "border", style.border, ui::S_BORDER, style.set); readStyleColor(state, 2, "face", style.face, ui::S_FACE, style.set); - readStyleColor(state, 2, "pressedFace", style.pressedFace, ui::S_PRESSED_FACE, style.set); - readStyleColor(state, 2, "pressedColor", style.pressedColor, ui::S_PRESSED_COLOR, style.set); - readStyleColor(state, 2, "focusColor", style.focusColor, ui::S_FOCUS_COLOR, style.set); + readStyleColor(state, 2, "pressedFace", style.pressedFace, ui::S_PRESSED_FACE, + style.set); + readStyleColor(state, 2, "pressedColor", style.pressedColor, + ui::S_PRESSED_COLOR, style.set); + readStyleColor(state, 2, "focusColor", style.focusColor, ui::S_FOCUS_COLOR, + style.set); readStyleColor(state, 2, "font", style.font, ui::S_FONT, style.set); - readStyleColor(state, 2, "textStyle", style.textStyle, ui::S_TEXT_STYLE, style.set); + readStyleColor(state, 2, "textStyle", style.textStyle, ui::S_TEXT_STYLE, + style.set); int32_t radius = style.radius; readStyleColor(state, 2, "radius", radius, ui::S_RADIUS, style.set); @@ -321,30 +352,37 @@ int setPressed(lua_State* state) { } int isPressed(lua_State* state) { - lua_pushboolean(state, (tree(state).nodes[checkNode(state, 1)].flags & ui::PRESSED) != 0); + lua_pushboolean( + state, (tree(state).nodes[checkNode(state, 1)].flags & ui::PRESSED) != 0); return 1; } uint16_t firstInteractive(const ui::Tree& nodes, uint16_t id) { - if (nodes.nodes[id].flags & ui::INTERACTIVE) return id; - for (uint16_t child = nodes.nodes[id].first; child != ui::NONE; child = nodes.nodes[child].next) { + if (nodes.nodes[id].flags & ui::INTERACTIVE) + return id; + for (uint16_t child = nodes.nodes[id].first; child != ui::NONE; + child = nodes.nodes[child].next) { const uint16_t found = firstInteractive(nodes, child); - if (found != ui::NONE) return found; + if (found != ui::NONE) + return found; } return ui::NONE; } void setFocusTo(ui::Tree& nodes, uint16_t id) { - if (nodes.focus != ui::NONE) nodes.nodes[nodes.focus].flags |= ui::DIRTY; + if (nodes.focus != ui::NONE) + nodes.nodes[nodes.focus].flags |= ui::DIRTY; nodes.focus = id; - if (id != ui::NONE) nodes.nodes[id].flags |= ui::DIRTY; + if (id != ui::NONE) + nodes.nodes[id].flags |= ui::DIRTY; } int focusFirst(lua_State* state) { ui::Tree& nodes = tree(state); const uint16_t found = firstInteractive(nodes, checkNode(state, 1)); setFocusTo(nodes, found); - if (found == ui::NONE) return 0; + if (found == ui::NONE) + return 0; lua_pushinteger(state, found); return 1; } @@ -357,16 +395,17 @@ int setFocus(lua_State* state) { int getFocus(lua_State* state) { const uint16_t focus = tree(state).focus; - if (focus == ui::NONE) return 0; + if (focus == ui::NONE) + return 0; lua_pushinteger(state, focus); return 1; } -// Nearest candidate strictly beyond the current node's edge, scored by the gap along the -// travel axis plus the misalignment across it, so a directly adjacent control always -// beats a distant one that happens to line up. -void collectCandidate(const ui::Tree& nodes, uint16_t id, const ui::Node& from, const char* direction, - uint16_t& best, long& bestScore) { +// Nearest candidate strictly beyond the current node's edge, scored by the gap +// along the travel axis plus the misalignment across it, so a directly adjacent +// control always beats a distant one that happens to line up. +void collectCandidate(const ui::Tree& nodes, uint16_t id, const ui::Node& from, + const char* direction, uint16_t& best, long& bestScore) { const ui::Node& node = nodes.nodes[id]; if ((node.flags & ui::INTERACTIVE) && id != nodes.focus) { const long dx = (node.x + node.w / 2) - (from.x + from.w / 2); @@ -399,7 +438,8 @@ void collectCandidate(const ui::Tree& nodes, uint16_t id, const ui::Node& from, } } } - for (uint16_t child = nodes.nodes[id].first; child != ui::NONE; child = nodes.nodes[child].next) { + for (uint16_t child = nodes.nodes[id].first; child != ui::NONE; + child = nodes.nodes[child].next) { collectCandidate(nodes, child, from, direction, best, bestScore); } } @@ -407,18 +447,22 @@ void collectCandidate(const ui::Tree& nodes, uint16_t id, const ui::Node& from, int moveFocus(lua_State* state) { const uint16_t root = checkNode(state, 1); const char* direction = luaL_checkstring(state, 2); - luaL_argcheck(state, - strcmp(direction, "up") == 0 || strcmp(direction, "down") == 0 || strcmp(direction, "left") == 0 || - strcmp(direction, "right") == 0, - 2, "expected up, down, left, or right"); + luaL_argcheck( + state, + strcmp(direction, "up") == 0 || strcmp(direction, "down") == 0 || + strcmp(direction, "left") == 0 || strcmp(direction, "right") == 0, + 2, "expected up, down, left, or right"); ui::Tree& nodes = tree(state); - if (nodes.focus == ui::NONE) return focusFirst(state); + if (nodes.focus == ui::NONE) + return focusFirst(state); uint16_t best = ui::NONE; long bestScore = 0; - collectCandidate(nodes, root, nodes.nodes[nodes.focus], direction, best, bestScore); - if (best != ui::NONE) setFocusTo(nodes, best); + collectCandidate(nodes, root, nodes.nodes[nodes.focus], direction, best, + bestScore); + if (best != ui::NONE) + setFocusTo(nodes, best); lua_pushinteger(state, nodes.focus); return 1; } @@ -436,7 +480,9 @@ void callPainter(void* context, uint16_t id, int x, int y, int w, int h) { lua_pushinteger(state, w); lua_pushinteger(state, h); if (lua_pcall(state, 5, 0, 0) != LUA_OK) { - Runtime::from(state)->log().write(LogLevel::Error, lua_tostring(state, -1) ? lua_tostring(state, -1) : "painter"); + Runtime::from(state)->log().write( + LogLevel::Error, + lua_tostring(state, -1) ? lua_tostring(state, -1) : "painter"); lua_pop(state, 1); } } @@ -545,15 +591,18 @@ const luaL_Reg FUNCTIONS[] = { // --- Returns the focused node. // @return NodeId|nil {"getFocus", getFocus}, - // --- Moves to the nearest interactive node in the requested direction without wrapping. + // --- Moves to the nearest interactive node in the requested direction + // without wrapping. // @param root NodeId // @param direction NodeDirection // @return NodeId|nil focused Current focus when no candidate exists. {"moveFocus", moveFocus}, // --- Registers the painter every custom node calls. - // @param painter fun(id: NodeId, x: integer, y: integer, w: integer, h: integer) + // @param painter fun(id: NodeId, x: integer, y: integer, w: integer, h: + // integer) {"setPainter", setPainter}, - // --- Paints dirty nodes; the firmware owns publication to the physical display. + // --- Paints dirty nodes; the firmware owns publication to the physical + // display. // @param root NodeId {"draw", draw}, // --- Returns the number of nodes in the tree. @@ -565,12 +614,12 @@ const luaL_Reg FUNCTIONS[] = { {nullptr, nullptr}, }; -} // namespace +} // namespace void registerNode(lua_State* state) { luaL_newlib(state, FUNCTIONS); lua_setglobal(state, "node"); } -} // namespace bindings -} // namespace esp32lua +} // namespace bindings +} // namespace esp32lua diff --git a/native/src/bindings/core/settings.cpp b/native/src/bindings/core/settings.cpp index b0a28ae..b69cecc 100644 --- a/native/src/bindings/core/settings.cpp +++ b/native/src/bindings/core/settings.cpp @@ -30,7 +30,8 @@ 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)); + return pushStatus(state, + Runtime::from(state)->settings().setRotation(degrees)); } int getTimezone(lua_State* state) { @@ -42,7 +43,8 @@ int getTimezone(lua_State* state) { 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})); + return pushStatus( + state, Runtime::from(state)->settings().setTimezone({value, length})); } const luaL_Reg FUNCTIONS[] = { @@ -65,12 +67,12 @@ const luaL_Reg FUNCTIONS[] = { {nullptr, nullptr}, }; -} // namespace +} // namespace void registerSettings(lua_State* state) { luaL_newlib(state, FUNCTIONS); lua_setglobal(state, "settings"); } -} // namespace bindings -} // namespace esp32lua +} // namespace bindings +} // namespace esp32lua diff --git a/native/src/bindings/core/sys.cpp b/native/src/bindings/core/sys.cpp index 69bde8f..713ab4d 100644 --- a/native/src/bindings/core/sys.cpp +++ b/native/src/bindings/core/sys.cpp @@ -7,23 +7,49 @@ namespace esp32lua { namespace bindings { namespace { -int getAPIVersion(lua_State* state) { lua_pushinteger(state, API_VERSION); return 1; } -int hasFeature(lua_State* state) { lua_pushboolean(state, Runtime::from(state)->hasFeature(luaL_checkstring(state, 1))); return 1; } -int getMillis(lua_State* state) { lua_pushinteger(state, Runtime::from(state)->sys().millis()); return 1; } -int getAppID(lua_State* state) { pushString(state, Runtime::from(state)->appId()); return 1; } -int getAppTitle(lua_State* state) { pushString(state, Runtime::from(state)->appTitle()); return 1; } -int getAppDataPath(lua_State* state) { pushString(state, Runtime::from(state)->appDataPath()); return 1; } -int setAppTitle(lua_State* state) { Runtime::from(state)->setAppTitle(luaL_checkstring(state, 1)); return 0; } +int getAPIVersion(lua_State* state) { + lua_pushinteger(state, API_VERSION); + return 1; +} +int hasFeature(lua_State* state) { + lua_pushboolean(state, + Runtime::from(state)->hasFeature(luaL_checkstring(state, 1))); + return 1; +} +int getMillis(lua_State* state) { + lua_pushinteger(state, Runtime::from(state)->sys().millis()); + return 1; +} +int getAppID(lua_State* state) { + pushString(state, Runtime::from(state)->appId()); + return 1; +} +int getAppTitle(lua_State* state) { + pushString(state, Runtime::from(state)->appTitle()); + return 1; +} +int getAppDataPath(lua_State* state) { + pushString(state, Runtime::from(state)->appDataPath()); + return 1; +} +int setAppTitle(lua_State* state) { + Runtime::from(state)->setAppTitle(luaL_checkstring(state, 1)); + return 0; +} int navigate(lua_State* state, bool replace) { const std::string path = checkString(state, 1); - const std::string arg = lua_isnoneornil(state, 2) ? std::string() : checkString(state, 2); + const std::string arg = + lua_isnoneornil(state, 2) ? std::string() : checkString(state, 2); Runtime::from(state)->requestLaunch(path, arg, replace); return 0; } int launch(lua_State* state) { return navigate(state, false); } int replace(lua_State* state) { return navigate(state, true); } -int back(lua_State* state) { Runtime::from(state)->requestBack(); return 0; } +int back(lua_State* state) { + Runtime::from(state)->requestBack(); + return 0; +} int getMemory(lua_State* state) { const MemoryInfo memory = Runtime::from(state)->sys().memory(); @@ -32,7 +58,10 @@ int getMemory(lua_State* state) { lua_pushinteger(state, memory.largestFreeBlock); return 3; } -int isClockSynced(lua_State* state) { lua_pushboolean(state, Runtime::from(state)->sys().isClockSynced()); return 1; } +int isClockSynced(lua_State* state) { + lua_pushboolean(state, Runtime::from(state)->sys().isClockSynced()); + return 1; +} const luaL_Reg FUNCTIONS[] = { // --- Returns the implemented API contract version. @@ -51,8 +80,10 @@ const luaL_Reg FUNCTIONS[] = { // --- Returns the running app title, initially the app ID. // @return string {"getAppTitle", getAppTitle}, - // --- Returns the current app's guaranteed-existing persistent data directory. - // @return string Absolute path under /.lua/data, preserved across app updates. + // --- Returns the current app's guaranteed-existing persistent data + // directory. + // @return string Absolute path under /.lua/data, preserved across app + // updates. {"getAppDataPath", getAppDataPath}, // --- Changes the running app's display title. // @param title string @@ -78,7 +109,10 @@ const luaL_Reg FUNCTIONS[] = { {nullptr, nullptr}, }; -} // namespace -void registerSys(lua_State* state) { luaL_newlib(state, FUNCTIONS); lua_setglobal(state, "sys"); } -} // namespace bindings -} // namespace esp32lua +} // namespace +void registerSys(lua_State* state) { + luaL_newlib(state, FUNCTIONS); + lua_setglobal(state, "sys"); +} +} // namespace bindings +} // namespace esp32lua diff --git a/native/src/bindings/core/timer.cpp b/native/src/bindings/core/timer.cpp index 8834702..ce10aa0 100644 --- a/native/src/bindings/core/timer.cpp +++ b/native/src/bindings/core/timer.cpp @@ -15,8 +15,10 @@ int schedule(lua_State* state, bool repeating) { lua_pushvalue(state, 2); const int callbackRef = luaL_ref(state, LUA_REGISTRYINDEX); - const TimerId id = Runtime::from(state)->addTimer(callbackRef, intervalMs, repeating); - if (id == 0) return luaL_error(state, "no timer slot available"); + const TimerId id = + Runtime::from(state)->addTimer(callbackRef, intervalMs, repeating); + if (id == 0) + return luaL_error(state, "no timer slot available"); lua_pushinteger(state, id); return 1; } @@ -31,12 +33,14 @@ int cancel(lua_State* state) { const luaL_Reg FUNCTIONS[] = { // --- Runs a callback once after a delay. - // @param intervalMs integer Positive delay; callback timing is best effort and never early. + // @param intervalMs integer Positive delay; callback timing is best effort + // and never early. // @param callback TimerCallback Retained until it fires or is cancelled. // @return TimerId {"after", after}, // --- Runs a callback repeatedly. - // @param intervalMs integer Positive interval; callback timing is best effort and never early. + // @param intervalMs integer Positive interval; callback timing is best + // effort and never early. // @param callback TimerCallback Retained until cancelled. // @return TimerId {"every", every}, @@ -47,12 +51,12 @@ const luaL_Reg FUNCTIONS[] = { {nullptr, nullptr}, }; -} // namespace +} // namespace void registerTimer(lua_State* state) { luaL_newlib(state, FUNCTIONS); lua_setglobal(state, "timer"); } -} // namespace bindings -} // namespace esp32lua +} // namespace bindings +} // namespace esp32lua diff --git a/native/src/bindings/core/wifi.cpp b/native/src/bindings/core/wifi.cpp index 147fe12..e26ebc9 100644 --- a/native/src/bindings/core/wifi.cpp +++ b/native/src/bindings/core/wifi.cpp @@ -4,7 +4,8 @@ // @lua-preamble ---@field rssi integer // @lua-preamble ---@field secure boolean // @lua-preamble -// @lua-preamble ---@alias WifiState "disconnected"|"connecting"|"connected"|"not_found"|"failed" +// @lua-preamble ---@alias WifiState +// "disconnected"|"connecting"|"connected"|"not_found"|"failed" // @lua-preamble // @lua-preamble ---@class WifiStatus // @lua-preamble ---@field state WifiState @@ -21,7 +22,8 @@ namespace { int scan(lua_State* state) { std::vector networks; const Status status = Runtime::from(state)->wifi().scan(networks); - if (!status.ok) return pushError(state, status.error); + if (!status.ok) + return pushError(state, status.error); lua_createtable(state, static_cast(networks.size()), 0); for (size_t at = 0; at < networks.size(); at++) { @@ -39,8 +41,8 @@ int connect(lua_State* state) { std::string password; const bool hasSsid = optionalString(state, 1, ssid); const bool hasPassword = optionalString(state, 2, password); - const Status status = - Runtime::from(state)->wifi().connect(hasSsid ? &ssid : nullptr, hasPassword ? &password : nullptr); + const Status status = Runtime::from(state)->wifi().connect( + hasSsid ? &ssid : nullptr, hasPassword ? &password : nullptr); return pushStatus(state, status); } @@ -55,7 +57,8 @@ int getStatus(lua_State* state) { } int isConnected(lua_State* state) { - lua_pushboolean(state, Runtime::from(state)->wifi().status().state == "connected"); + lua_pushboolean(state, + Runtime::from(state)->wifi().status().state == "connected"); return 1; } @@ -70,14 +73,17 @@ int disconnect(lua_State* state) { return 0; } -int forget(lua_State* state) { return pushStatus(state, Runtime::from(state)->wifi().forget()); } +int forget(lua_State* state) { + return pushStatus(state, Runtime::from(state)->wifi().forget()); +} const luaL_Reg FUNCTIONS[] = { // --- Scans for visible networks. // @return WifiNetwork[]|nil networks // @return string|nil error {"scan", scan}, - // --- With credentials, saves and joins that network. Without them, reconnects saved credentials. + // --- With credentials, saves and joins that network. Without them, + // reconnects saved credentials. // @param ssid string|nil // @param password string|nil Omit for an open network. // @return true|nil ok @@ -101,12 +107,12 @@ const luaL_Reg FUNCTIONS[] = { {nullptr, nullptr}, }; -} // namespace +} // namespace void registerWifi(lua_State* state) { luaL_newlib(state, FUNCTIONS); lua_setglobal(state, "wifi"); } -} // namespace bindings -} // namespace esp32lua +} // namespace bindings +} // namespace esp32lua diff --git a/native/src/bindings/features/buttons.cpp b/native/src/bindings/features/buttons.cpp index b584420..d3c3728 100644 --- a/native/src/bindings/features/buttons.cpp +++ b/native/src/bindings/features/buttons.cpp @@ -35,10 +35,13 @@ int wasReleased(lua_State* state) { // @lua-augment input InputLib // @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 takes. +// @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 +// takes. const luaL_Reg INPUT_FUNCTIONS[] = { - // ---Returns the roles this device reports, so an app can label only the actions it has. + // ---Returns the roles this device reports, so an app can label only the + // actions it has. // @return Button[] {"getButtons", getButtons}, // ---Whether any button is held. @@ -59,12 +62,14 @@ const luaL_Reg INPUT_FUNCTIONS[] = { {nullptr, nullptr}, }; -} // namespace +} // namespace -void registerButtons(lua_State* state) { augmentGlobal(state, "input", INPUT_FUNCTIONS); } +void registerButtons(lua_State* state) { + augmentGlobal(state, "input", INPUT_FUNCTIONS); +} -} // namespace bindings -} // namespace esp32lua +} // namespace bindings +} // namespace esp32lua // @lua-global // ---Fired when a button goes down. diff --git a/native/src/bindings/features/touch.cpp b/native/src/bindings/features/touch.cpp index 232ef7a..c790b3e 100644 --- a/native/src/bindings/features/touch.cpp +++ b/native/src/bindings/features/touch.cpp @@ -9,11 +9,13 @@ int setCalibration(lua_State* state) { const int32_t y0 = checkInt(state, 2); const int32_t x1 = checkInt(state, 3); const int32_t y1 = checkInt(state, 4); - return pushStatus(state, Runtime::from(state)->touch().setCalibration(x0, y0, x1, y1)); + return pushStatus( + state, Runtime::from(state)->touch().setCalibration(x0, y0, x1, y1)); } int pushPoint(lua_State* state, bool touched, int32_t x, int32_t y) { - if (!touched) return 0; + if (!touched) + return 0; lua_pushinteger(state, x); lua_pushinteger(state, y); return 2; @@ -53,11 +55,13 @@ const luaL_Reg SETTINGS_FUNCTIONS[] = { // @lua-augment input InputLib const luaL_Reg INPUT_FUNCTIONS[] = { - // --- Returns the calibrated touch point, or nothing when the panel is not touched. + // --- 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. + // --- Returns the uncalibrated touch reading, or nothing when the panel is + // not touched. // @return integer|nil x // @return integer|nil y {"getRawTouch", getRawTouch}, @@ -67,22 +71,23 @@ const luaL_Reg INPUT_FUNCTIONS[] = { {nullptr, nullptr}, }; -} // namespace +} // namespace void registerTouch(lua_State* state) { augmentGlobal(state, "settings", SETTINGS_FUNCTIONS); augmentGlobal(state, "input", INPUT_FUNCTIONS); } -} // namespace bindings -} // namespace esp32lua +} // namespace bindings +} // namespace esp32lua // @lua-global // ---Fired when the finger lands. // @param x integer // @param y integer // @lua-fn on_touch_down -// ---Fired when the finger moves while down, after the firmware's jitter filter. +// ---Fired when the finger moves while down, after the firmware's jitter +// filter. // @param x integer // @param y integer // @lua-fn on_touch_move diff --git a/native/src/bindings/helpers.h b/native/src/bindings/helpers.h index 4b8e429..f0c5cc8 100644 --- a/native/src/bindings/helpers.h +++ b/native/src/bindings/helpers.h @@ -1,8 +1,8 @@ #pragma once -// Argument and result marshalling shared by the binding sources. Every luaL_check* call -// happens before a non-trivial C++ local exists, because a Lua error longjmps past -// destructors. +// Argument and result marshalling shared by the binding sources. Every +// luaL_check* call happens before a non-trivial C++ local exists, because a Lua +// error longjmps past destructors. #include #include @@ -29,7 +29,8 @@ inline std::string checkString(lua_State* state, int index) { } inline bool optionalString(lua_State* state, int index, std::string& out) { - if (lua_isnoneornil(state, index)) return false; + if (lua_isnoneornil(state, index)) + return false; out = checkString(state, index); return true; } @@ -43,12 +44,14 @@ inline int32_t optionalInt(lua_State* state, int index, int32_t fallback) { } inline bool optionalColor(lua_State* state, int index, int32_t& out) { - if (lua_isnoneornil(state, index)) return false; + if (lua_isnoneornil(state, index)) + return false; out = checkInt(state, index); return true; } -// `true` on success, `nil, error` on failure: the shape every mutating binding returns. +// `true` on success, `nil, error` on failure: the shape every mutating binding +// returns. inline int pushStatus(lua_State* state, const Status& status) { if (status.ok) { lua_pushboolean(state, true); @@ -65,7 +68,8 @@ inline int pushError(lua_State* state, const std::string& error) { return 2; } -inline void pushStrings(lua_State* state, const std::vector& values) { +inline void pushStrings(lua_State* state, + const std::vector& values) { lua_createtable(state, static_cast(values.size()), 0); for (size_t index = 0; index < values.size(); index++) { pushString(state, values[index]); @@ -73,7 +77,8 @@ inline void pushStrings(lua_State* state, const std::vector& values } } -inline void setField(lua_State* state, const char* key, const std::string& value) { +inline void setField(lua_State* state, const char* key, + const std::string& value) { pushString(state, value); lua_setfield(state, -2, key); } @@ -88,8 +93,10 @@ inline void setField(lua_State* state, const char* key, bool value) { lua_setfield(state, -2, key); } -// Feature contracts add functions to namespaces the core registrations already created. -inline void augmentGlobal(lua_State* state, const char* name, const luaL_Reg* functions) { +// Feature contracts add functions to namespaces the core registrations already +// created. +inline void augmentGlobal(lua_State* state, const char* name, + const luaL_Reg* functions) { lua_getglobal(state, name); if (!lua_istable(state, -1)) { lua_pop(state, 1); @@ -101,5 +108,5 @@ inline void augmentGlobal(lua_State* state, const char* name, const luaL_Reg* fu lua_pop(state, 1); } -} // namespace bindings -} // namespace esp32lua +} // namespace bindings +} // namespace esp32lua diff --git a/native/src/node/painter.h b/native/src/node/painter.h index f76e757..5e2f8d6 100644 --- a/native/src/node/painter.h +++ b/native/src/node/painter.h @@ -1,9 +1,10 @@ #pragma once -// Painting the node tree through GuiProvider, so the same walk drives an LCD and an -// e-ink panel. A dirty node paints itself and dirties its children, because a parent's -// fill lands on top of whatever they drew; nothing tracks sub-regions, and a widget that -// wants to repaint part of itself is a CUSTOM node painting through the gui bindings. +// Painting the node tree through GuiProvider, so the same walk drives an LCD +// and an e-ink panel. A dirty node paints itself and dirties its children, +// because a parent's fill lands on top of whatever they drew; nothing tracks +// sub-regions, and a widget that wants to repaint part of itself is a CUSTOM +// node painting through the gui bindings. #include #include @@ -11,10 +12,11 @@ namespace esp32lua { namespace ui { -typedef void (*CustomPainter)(void* context, uint16_t id, int x, int y, int w, int h); +typedef void (*CustomPainter)(void* context, uint16_t id, int x, int y, int w, + int h); class Painter { - public: +public: Painter(GuiProvider& gui, Tree& tree) : gui(gui), tree(tree) {} CustomPainter custom = nullptr; @@ -24,51 +26,58 @@ class Painter { if (tree.nodes[id].flags & DIRTY) { paint(id); tree.nodes[id].flags &= ~DIRTY; - for (uint16_t c = tree.nodes[id].first; c != NONE; c = tree.nodes[c].next) { + for (uint16_t c = tree.nodes[id].first; c != NONE; + c = tree.nodes[c].next) { tree.nodes[c].flags |= DIRTY; } } - for (uint16_t c = tree.nodes[id].first; c != NONE; c = tree.nodes[c].next) draw(c); + for (uint16_t c = tree.nodes[id].first; c != NONE; c = tree.nodes[c].next) + draw(c); } - private: +private: GuiProvider& gui; Tree& tree; - // What a node sits on, which is not what it fills. Derived rather than stored, because - // a node cannot be told what is behind it: a dialog layer paints nothing, so its card - // blends into the dimmed content two levels up, not into the lit palette the layer - // hands its children. + // What a node sits on, which is not what it fills. Derived rather than + // stored, because a node cannot be told what is behind it: a dialog layer + // paints nothing, so its card blends into the dimmed content two levels up, + // not into the lit palette the layer hands its children. int32_t surfaceOf(uint16_t id) const { - for (uint16_t n = tree.nodes[id].parent; n != NONE; n = tree.nodes[n].parent) { + for (uint16_t n = tree.nodes[id].parent; n != NONE; + n = tree.nodes[n].parent) { const Style* style = tree.styleOf(n); - if (style && (style->set & S_FILL)) return style->fill; + if (style && (style->set & S_FILL)) + return style->fill; } uint16_t root = id; - while (tree.nodes[root].parent != NONE) root = tree.nodes[root].parent; + while (tree.nodes[root].parent != NONE) + root = tree.nodes[root].parent; return tree.inherited(root, S_BG).bg; } void paint(uint16_t id) { const Node& n = tree.nodes[id]; switch (n.type) { - case BUTTON: - paintButton(id); - break; - case TEXT: - paintText(id); - break; - case CUSTOM: - // Cleared first, because a custom painter draws what it wants and nothing knows - // what it drew last time. - gui.fillRect(n.x, n.y, n.w, n.h, tree.inherited(id, S_BG).bg); - if (custom) custom(context, id, n.x, n.y, n.w, n.h); - break; - default: - paintBox(id); - break; + case BUTTON: + paintButton(id); + break; + case TEXT: + paintText(id); + break; + case CUSTOM: + // Cleared first, because a custom painter draws what it wants and nothing + // knows what it drew last time. + gui.fillRect(n.x, n.y, n.w, n.h, tree.inherited(id, S_BG).bg); + if (custom) + custom(context, id, n.x, n.y, n.w, n.h); + break; + default: + paintBox(id); + break; } - if (id == tree.focus) paintFocus(id); + if (id == tree.focus) + paintFocus(id); } void paintBox(uint16_t id) { @@ -76,39 +85,44 @@ class Painter { const Style* own = tree.styleOf(id); const bool hasBorder = own && (own->set & S_BORDER); const bool hasFill = own && (own->set & S_FILL); - if (!hasFill && !hasBorder) return; + if (!hasFill && !hasBorder) + return; - // Only a bordered box rounds its corners. Filling a square first would leave corners - // outside the border, and rounding an unbordered fill puts a seam where a plain panel - // background was expected. + // Only a bordered box rounds its corners. Filling a square first would + // leave corners outside the border, and rounding an unbordered fill puts a + // seam where a plain panel background was expected. if (!hasBorder) { gui.fillRect(n.x, n.y, n.w, n.h, own->fill); return; } const int32_t fill = hasFill ? own->fill : tree.inherited(id, S_BG).bg; - gui.roundRect(n.x, n.y, n.w, n.h, tree.inherited(id, S_RADIUS).radius, surfaceOf(id), &fill, &fill, - &own->border); + gui.roundRect(n.x, n.y, n.w, n.h, tree.inherited(id, S_RADIUS).radius, + surfaceOf(id), &fill, &fill, &own->border); } void paintButton(uint16_t id) { const Node& n = tree.nodes[id]; const bool pressed = (n.flags & PRESSED) != 0; - const Style& faceStyle = tree.inherited(id, pressed ? S_PRESSED_FACE : S_FACE); + const Style& faceStyle = + tree.inherited(id, pressed ? S_PRESSED_FACE : S_FACE); const int32_t face = pressed ? faceStyle.pressedFace : faceStyle.face; const int32_t radius = tree.inherited(id, S_RADIUS).radius; const Style* own = tree.styleOf(id); const bool hasBorder = own && (own->set & S_BORDER); - gui.roundRect(n.x, n.y, n.w, n.h, radius, surfaceOf(id), &face, &face, hasBorder ? &own->border : nullptr); + gui.roundRect(n.x, n.y, n.w, n.h, radius, surfaceOf(id), &face, &face, + hasBorder ? &own->border : nullptr); } - // Glyphs over a button are transparent: an opaque fill is one flat colour, and the - // face is repainted whenever it changes, so the label has nothing to erase. Elsewhere - // the whole box is cleared, because a label replaced by a shorter one would otherwise - // leave the tail of the old text standing next to the new. + // Glyphs over a button are transparent: an opaque fill is one flat colour, + // and the face is repainted whenever it changes, so the label has nothing to + // erase. Elsewhere the whole box is cleared, because a label replaced by a + // shorter one would otherwise leave the tail of the old text standing next to + // the new. void paintText(uint16_t id) { const Node& n = tree.nodes[id]; const char* label = tree.label(id); - if (!label) return; + if (!label) + return; const uint16_t parent = n.parent; const bool onButton = parent != NONE && tree.nodes[parent].type == BUTTON; @@ -117,7 +131,9 @@ class Painter { const int32_t textStyle = tree.inherited(id, S_TEXT_STYLE).textStyle; if (pressed) { - gui.drawText(font, n.x, n.y, label, tree.inherited(id, S_PRESSED_COLOR).pressedColor, textStyle, nullptr); + gui.drawText(font, n.x, n.y, label, + tree.inherited(id, S_PRESSED_COLOR).pressedColor, textStyle, + nullptr); return; } const int32_t color = tree.inherited(id, S_COLOR).color; @@ -134,11 +150,12 @@ class Painter { void paintFocus(uint16_t id) { const Node& n = tree.nodes[id]; const Style& style = tree.inherited(id, S_FOCUS_COLOR); - if (!(style.set & S_FOCUS_COLOR)) return; - gui.roundRect(n.x, n.y, n.w, n.h, tree.inherited(id, S_RADIUS).radius, surfaceOf(id), nullptr, nullptr, - &style.focusColor); + if (!(style.set & S_FOCUS_COLOR)) + return; + gui.roundRect(n.x, n.y, n.w, n.h, tree.inherited(id, S_RADIUS).radius, + surfaceOf(id), nullptr, nullptr, &style.focusColor); } }; -} // namespace ui -} // namespace esp32lua +} // namespace ui +} // namespace esp32lua diff --git a/native/src/runtime/loader.cpp b/native/src/runtime/loader.cpp index 7a93eeb..3c0094c 100644 --- a/native/src/runtime/loader.cpp +++ b/native/src/runtime/loader.cpp @@ -1,8 +1,9 @@ -// Loading Lua off the SD card. Lua's stock loaders go through stdio, which cannot see the -// mount, so every path into the filesystem goes through FsProvider instead. +// Loading Lua off the SD card. Lua's stock loaders go through stdio, which +// cannot see the mount, so every path into the filesystem goes through +// FsProvider instead. -#include #include +#include #include @@ -21,12 +22,14 @@ struct ChunkReader { const char* readChunk(lua_State*, void* context, size_t* size) { ChunkReader* reader = static_cast(context); - const int32_t read = reader->file->read(reader->buffer, sizeof(reader->buffer)); + const int32_t read = + reader->file->read(reader->buffer, sizeof(reader->buffer)); *size = read > 0 ? static_cast(read) : 0; return read > 0 ? reader->buffer : nullptr; } -// Loads a path onto the stack as a chunk, or pushes nothing and returns a Lua status. +// Loads a path onto the stack as a chunk, or pushes nothing and returns a Lua +// status. int load(lua_State* state, FsProvider& fs, const std::string& path) { ChunkReader reader; reader.file = fs.openRead(path); @@ -35,24 +38,26 @@ int load(lua_State* state, FsProvider& fs, const std::string& path) { return LUA_ERRFILE; } const std::string chunkname = "@" + path; - const int status = lua_load(state, readChunk, &reader, chunkname.c_str(), "t"); + const int status = + lua_load(state, readChunk, &reader, chunkname.c_str(), "t"); delete reader.file; return status; } -} // namespace +} // namespace bool Runtime::loadScript(const std::string& path) { return load(state_, *providers_.fs, path) == LUA_OK; } -// Resolves a module name against package.path, reporting every path tried the way the stock -// searcher does. +// Resolves a module name against package.path, reporting every path tried the +// way the stock searcher does. int Runtime::searchModule(lua_State* state) { Runtime* runtime = Runtime::from(state); std::string name = luaL_checkstring(state, 1); for (size_t at = 0; at < name.size(); at++) { - if (name[at] == '.') name[at] = '/'; + if (name[at] == '.') + name[at] = '/'; } lua_getglobal(state, "package"); @@ -64,19 +69,23 @@ int Runtime::searchModule(lua_State* state) { size_t start = 0; while (start <= templates.size()) { const size_t end = templates.find(';', start); - std::string candidate = templates.substr(start, end == std::string::npos ? std::string::npos : end - start); + std::string candidate = templates.substr( + start, end == std::string::npos ? std::string::npos : end - start); start = end == std::string::npos ? templates.size() + 1 : end + 1; - if (candidate.empty()) continue; + if (candidate.empty()) + continue; const size_t mark = candidate.find('?'); - if (mark != std::string::npos) candidate.replace(mark, 1, name); + if (mark != std::string::npos) + candidate.replace(mark, 1, name); if (!runtime->providers_.fs->exists(candidate)) { tried += "\n\tno file '" + candidate + "'"; continue; } if (load(state, *runtime->providers_.fs, candidate) != LUA_OK) { - return luaL_error(state, "error loading module '%s' from '%s':\n\t%s", luaL_checkstring(state, 1), - candidate.c_str(), lua_tostring(state, -1)); + return luaL_error(state, "error loading module '%s' from '%s':\n\t%s", + luaL_checkstring(state, 1), candidate.c_str(), + lua_tostring(state, -1)); } lua_pushstring(state, candidate.c_str()); return 2; @@ -88,11 +97,14 @@ int Runtime::searchModule(lua_State* state) { int Runtime::searchEmbedded(lua_State* state) { const char* name = luaL_checkstring(state, 1); for (size_t i = 0; i < embedded_modules_count; i++) { - if (std::strcmp(name, embedded_modules[i].name) != 0) continue; + if (std::strcmp(name, embedded_modules[i].name) != 0) + continue; const std::string chunkname = std::string("=") + name; - if (luaL_loadbuffer(state, reinterpret_cast(embedded_modules[i].data), - embedded_modules[i].size, chunkname.c_str()) != LUA_OK) { - return luaL_error(state, "error loading embedded module '%s': %s", name, lua_tostring(state, -1)); + if (luaL_loadbuffer( + state, reinterpret_cast(embedded_modules[i].data), + embedded_modules[i].size, chunkname.c_str()) != LUA_OK) { + return luaL_error(state, "error loading embedded module '%s': %s", name, + lua_tostring(state, -1)); } lua_pushstring(state, name); return 2; @@ -103,7 +115,9 @@ int Runtime::searchEmbedded(lua_State* state) { int Runtime::loadFile(lua_State* state) { Runtime* runtime = Runtime::from(state); - if (load(state, *runtime->providers_.fs, luaL_checkstring(state, 1)) == LUA_OK) return 1; + if (load(state, *runtime->providers_.fs, luaL_checkstring(state, 1)) == + LUA_OK) + return 1; lua_pushnil(state); lua_insert(state, -2); return 2; @@ -116,13 +130,13 @@ void Runtime::installLoader(const std::string& appDir) { 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. + // 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"); lua_pushcfunction(state_, searchModule); lua_rawseti(state_, -2, 2); - // Embedded bytecode is the fallback after the SD card, so a local /.lua/lib/ui.lua shadows - // the packaged one without reflashing. + // Embedded bytecode is the fallback after the SD card, so a local + // /.lua/lib/ui.lua shadows the packaged one without reflashing. lua_pushcfunction(state_, searchEmbedded); lua_rawseti(state_, -2, 3); lua_pushnil(state_); @@ -133,4 +147,4 @@ void Runtime::installLoader(const std::string& appDir) { lua_setglobal(state_, "loadfile"); } -} // namespace esp32lua +} // namespace esp32lua diff --git a/native/src/runtime/runtime.cpp b/native/src/runtime/runtime.cpp index 64b912b..71ca5b9 100644 --- a/native/src/runtime/runtime.cpp +++ b/native/src/runtime/runtime.cpp @@ -20,39 +20,48 @@ void registerSys(lua_State* state); void registerTimer(lua_State* state); void registerTouch(lua_State* state); void registerWifi(lua_State* state); -} // namespace bindings +} // namespace bindings namespace { -// App routes are relative and stay inside the apps root, so a traversal component is a hard no. +// App routes are relative and stay inside the apps root, so a traversal +// component is a hard no. bool isSafeRoute(const std::string& path) { - if (path.empty() || path[0] == '/') return false; + if (path.empty() || path[0] == '/') + return false; size_t start = 0; while (start <= path.size()) { const size_t end = path.find('/', start); - const std::string part = path.substr(start, end == std::string::npos ? std::string::npos : end - start); - if (part.empty() || part == "." || part == "..") return false; - if (end == std::string::npos) break; + const std::string part = path.substr( + start, end == std::string::npos ? std::string::npos : end - start); + if (part.empty() || part == "." || part == "..") + return false; + if (end == std::string::npos) + break; start = end + 1; } return true; } -} // namespace +} // namespace -Runtime::Runtime(const Providers& providers, const Paths& paths) : providers_(providers), paths_(paths) {} +Runtime::Runtime(const Providers& providers, const Paths& paths) + : providers_(providers), paths_(paths) {} 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 (state_) + return true; + if (!providers_.log || !providers_.settings || !providers_.sys || + !providers_.fs || !providers_.gui || !providers_.http || + !providers_.timer || !providers_.wifi || !providers_.ble) { return false; } state_ = luaL_newstate(); - if (!state_) return false; + if (!state_) + return false; *static_cast(lua_getextraspace(state_)) = this; luaL_openlibs(state_); @@ -67,57 +76,74 @@ bool Runtime::open() { bindings::registerTimer(state_); bindings::registerWifi(state_); - // Feature namespaces extend the tables the core registrations just created, so they - // always follow them. - if (providers_.touch) bindings::registerTouch(state_); - if (providers_.buttons) bindings::registerButtons(state_); + // Feature namespaces extend the tables the core registrations just created, + // so they always follow them. + if (providers_.touch) + bindings::registerTouch(state_); + if (providers_.buttons) + bindings::registerButtons(state_); return true; } void Runtime::close() { - if (!state_) return; + if (!state_) + return; 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. + // Node handles mean nothing to the next lua_State, so an app that inherited + // the previous tree would build onto its nodes. tree_.reset(); appPath_.clear(); appTitle_.clear(); } -Runtime::Batch::Batch(Runtime& runtime) : runtime_(runtime) { runtime_.batchDepth_++; } +Runtime::Batch::Batch(Runtime& runtime) : runtime_(runtime) { + runtime_.batchDepth_++; +} Runtime::Batch::~Batch() { - if (--runtime_.batchDepth_ == 0) runtime_.providers_.gui->commit(); + if (--runtime_.batchDepth_ == 0) + runtime_.providers_.gui->commit(); } bool Runtime::beginCall(const char* name) { lua_getglobal(state_, name); - if (lua_isfunction(state_, -1)) return true; + if (lua_isfunction(state_, -1)) + return true; 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; + if (lua_pcall(state_, argc, 0, 0) == LUA_OK) + return true; const char* message = lua_tostring(state_, -1); - providers_.log->write(LogLevel::Error, std::string(name) + ": " + (message ? message : "failed")); + providers_.log->write(LogLevel::Error, std::string(name) + ": " + + (message ? message : "failed")); lua_pop(state_, 1); return false; } // @lua-global core/runtime // @lua-preamble -- Runtime layout: -// @lua-preamble -- /.lua/apps//main.lua application entry point -// @lua-preamble -- /.lua/apps///main.lua nested route, omitted from the launcher -// @lua-preamble -- /.lua/data// persistent app data, preserved across updates -// @lua-preamble -- /.lua/lib/.lua shared require() modules -// @lua-preamble -- require() also searches the running application's directory +// @lua-preamble -- /.lua/apps//main.lua application entry +// point +// @lua-preamble -- /.lua/apps///main.lua nested route, +// omitted from the launcher +// @lua-preamble -- /.lua/data// persistent app +// data, preserved across updates +// @lua-preamble -- /.lua/lib/.lua shared require() +// modules +// @lua-preamble -- require() also searches the running application's +// directory // @lua-preamble -- -// @lua-preamble -- The firmware does not clear the frame before calling draw(), and commits changed -// @lua-preamble -- display content after each callback batch using the panel's own refresh policy. -// @lua-preamble -- Timer callbacks are registered directly with timer.after/every. +// @lua-preamble -- The firmware does not clear the frame before calling draw(), +// and commits changed +// @lua-preamble -- display content after each callback batch using the panel's +// own refresh policy. +// @lua-preamble -- Timer callbacks are registered directly with +// timer.after/every. // ---Required. Runs once before the first draw; failing here stops the app. // @param arg string|nil The string passed to sys.launch or sys.replace. @@ -132,32 +158,41 @@ bool Runtime::callInit(const std::string& arg) { return finishCall("init", 1); } -// ---Optional frame loop, called once after init and then at most 30 FPS, best effort. -// @param deltaMs integer Monotonic milliseconds since the previous draw; zero on the first. +// ---Optional frame loop, called once after init and then at most 30 FPS, best +// effort. +// @param deltaMs integer Monotonic milliseconds since the previous draw; zero +// on the first. // @lua-fn draw void Runtime::callDraw(int32_t deltaMs) { const Batch batch(*this); - if (!beginCall("draw")) return; + if (!beginCall("draw")) + return; lua_pushinteger(state_, deltaMs); finishCall("draw", 1); } void Runtime::callTouch(TouchPhase phase, int32_t x, int32_t y) { if (!providers_.touch) { - providers_.log->write(LogLevel::Error, "callTouch without a touch provider"); + providers_.log->write(LogLevel::Error, + "callTouch without a touch provider"); return; } const Batch batch(*this); - const char* name = phase == TouchPhase::Down ? "on_touch_down" : (phase == TouchPhase::Move ? "on_touch_move" - : "on_touch_up"); + const char* name = + phase == TouchPhase::Down + ? "on_touch_down" + : (phase == TouchPhase::Move ? "on_touch_move" : "on_touch_up"); for (int pass = 0; pass < 2; pass++) { - // The tap alias is ordering, not policy: a release always fires on_touch_up and then - // on_touch, so both firmwares agree without either of them deciding anything. + // The tap alias is ordering, not policy: a release always fires on_touch_up + // and then on_touch, so both firmwares agree without either of them + // deciding anything. if (pass == 1) { - if (phase != TouchPhase::Up) return; + if (phase != TouchPhase::Up) + return; name = "on_touch"; } - if (!beginCall(name)) continue; + if (!beginCall(name)) + continue; lua_pushinteger(state_, x); lua_pushinteger(state_, y); finishCall(name, 2); @@ -166,17 +201,20 @@ void Runtime::callTouch(TouchPhase phase, int32_t x, int32_t y) { void Runtime::callButton(const std::string& button, bool pressed) { if (!providers_.buttons) { - providers_.log->write(LogLevel::Error, "callButton without a buttons provider"); + providers_.log->write(LogLevel::Error, + "callButton without a buttons provider"); return; } const Batch batch(*this); const char* name = pressed ? "on_button_down" : "on_button_up"; for (int pass = 0; pass < 2; pass++) { if (pass == 1) { - if (pressed) return; + if (pressed) + return; name = "on_button"; } - if (!beginCall(name)) continue; + if (!beginCall(name)) + continue; lua_pushlstring(state_, button.data(), button.size()); finishCall(name, 1); } @@ -196,7 +234,8 @@ TimerId Runtime::addTimer(int callbackRef, int32_t intervalMs, bool repeating) { bool Runtime::cancelTimer(TimerId id) { const std::map::iterator found = timers_.find(id); - if (found == timers_.end()) return false; + if (found == timers_.end()) + return false; providers_.timer->cancel(id); luaL_unref(state_, LUA_REGISTRYINDEX, found->second.callbackRef); timers_.erase(found); @@ -205,25 +244,31 @@ bool Runtime::cancelTimer(TimerId id) { void Runtime::callTimer(TimerId id) { const std::map::iterator found = timers_.find(id); - if (found == timers_.end()) return; + if (found == timers_.end()) + return; const Batch batch(*this); const int callbackRef = found->second.callbackRef; const bool repeating = found->second.repeating; - // A one-shot is forgotten before it runs, so a callback that cancels itself or starts a - // new timer sees a consistent table. - if (!repeating) timers_.erase(found); + // A one-shot is forgotten before it runs, so a callback that cancels itself + // or starts a new timer sees a consistent table. + if (!repeating) + timers_.erase(found); lua_rawgeti(state_, LUA_REGISTRYINDEX, callbackRef); if (lua_pcall(state_, 0, 0, 0) != LUA_OK) { - providers_.log->write(LogLevel::Error, lua_tostring(state_, -1) ? lua_tostring(state_, -1) : "timer failed"); + providers_.log->write(LogLevel::Error, lua_tostring(state_, -1) + ? lua_tostring(state_, -1) + : "timer failed"); lua_pop(state_, 1); } - if (!repeating) luaL_unref(state_, LUA_REGISTRYINDEX, callbackRef); + if (!repeating) + luaL_unref(state_, LUA_REGISTRYINDEX, callbackRef); } void Runtime::cancelAllTimers() { - for (std::map::iterator it = timers_.begin(); it != timers_.end(); ++it) { + for (std::map::iterator it = timers_.begin(); + it != timers_.end(); ++it) { providers_.timer->cancel(it->first); luaL_unref(state_, LUA_REGISTRYINDEX, it->second.callbackRef); } @@ -238,8 +283,10 @@ std::string Runtime::appId() const { std::string Runtime::appDataPath() const { return paths_.data + "/" + appId(); } bool Runtime::hasFeature(const std::string& feature) const { - if (feature == "touch") return providers_.touch != nullptr; - if (feature == "buttons") return providers_.buttons != nullptr; + if (feature == "touch") + return providers_.touch != nullptr; + if (feature == "buttons") + return providers_.buttons != nullptr; return false; } @@ -250,18 +297,22 @@ bool Runtime::startApp(const std::string& path, const std::string& arg) { } close(); - if (!open()) return false; + if (!open()) + return false; appPath_ = path; appTitle_ = appId(); const std::string directory = paths_.apps + "/" + path; installLoader(directory); if (!loadScript(directory + "/main.lua")) { - providers_.log->write(LogLevel::Error, lua_tostring(state_, -1) ? lua_tostring(state_, -1) : "load failed"); + providers_.log->write(LogLevel::Error, lua_tostring(state_, -1) + ? lua_tostring(state_, -1) + : "load failed"); close(); return false; } - // The chunk body runs first, then init(), so an app that fails either way leaves nothing behind. + // 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(); return false; @@ -269,7 +320,8 @@ bool Runtime::startApp(const std::string& path, const std::string& arg) { return true; } -void Runtime::requestLaunch(const std::string& path, const std::string& arg, bool replace) { +void Runtime::requestLaunch(const std::string& path, const std::string& arg, + bool replace) { pending_.kind = replace ? Pending::Replace : Pending::Launch; pending_.route.path = path; pending_.route.arg = arg; @@ -283,7 +335,8 @@ void Runtime::requestBack() { bool Runtime::applyPendingNavigation() { const Pending pending = pending_; pending_ = Pending(); - if (pending.kind == Pending::None) return hasApp(); + if (pending.kind == Pending::None) + return hasApp(); if (pending.kind == Pending::Back) { // An empty history means the launcher, which is an app like any other. @@ -304,6 +357,8 @@ bool Runtime::applyPendingNavigation() { return startApp(pending.route.path, pending.route.arg); } -Runtime* Runtime::from(lua_State* state) { return *static_cast(lua_getextraspace(state)); } +Runtime* Runtime::from(lua_State* state) { + return *static_cast(lua_getextraspace(state)); +} -} // namespace esp32lua +} // namespace esp32lua diff --git a/native/test/fake_providers.h b/native/test/fake_providers.h index 062dfb4..08f2811 100644 --- a/native/test/fake_providers.h +++ b/native/test/fake_providers.h @@ -1,7 +1,8 @@ #pragma once -// Host doubles for every provider, deliberately dumb: they record what a binding asked -// for and hand back canned values, so a test asserts the marshalling rather than a device. +// Host doubles for every provider, deliberately dumb: they record what a +// binding asked for and hand back canned values, so a test asserts the +// marshalling rather than a device. #include #include @@ -56,7 +57,9 @@ struct Fs : FsProvider { size_t at = 0; int32_t read(char* out, int32_t maxBytes) override { const size_t remaining = content.size() - at; - const size_t count = remaining < static_cast(maxBytes) ? remaining : static_cast(maxBytes); + const size_t count = remaining < static_cast(maxBytes) + ? remaining + : static_cast(maxBytes); content.copy(out, count, at); at += count; return static_cast(count); @@ -64,47 +67,65 @@ struct Fs : FsProvider { }; FileReader* openRead(const std::string& path) override { - const std::map::const_iterator found = files.find(path); - if (found == files.end()) return nullptr; + const std::map::const_iterator found = + files.find(path); + if (found == files.end()) + return nullptr; Reader* reader = new Reader(); reader->content = found->second; return reader; } - bool exists(const std::string& path) const override { return files.count(path) != 0; } + bool exists(const std::string& path) const override { + return files.count(path) != 0; + } Status fileSize(const std::string& path, int32_t& size) const override { - const std::map::const_iterator found = files.find(path); - if (found == files.end()) return Status::failure("no such file"); + const std::map::const_iterator found = + files.find(path); + if (found == files.end()) + return Status::failure("no such file"); size = static_cast(found->second.size()); return Status::success(); } - Status listDirs(const std::string&, std::vector& names) const override { + Status listDirs(const std::string&, + std::vector& names) const override { names.push_back("apps"); return Status::success(); } - Status listFiles(const std::string&, std::vector& names) const override { + Status listFiles(const std::string&, + std::vector& names) const override { names.push_back("main.lua"); return Status::success(); } Status mkdir(const std::string&) override { return Status::success(); } - Status readFile(const std::string& path, int32_t maxBytes, std::string& content) const override { - const std::map::const_iterator found = files.find(path); - if (found == files.end()) return Status::failure("no such file"); - if (static_cast(found->second.size()) > maxBytes) return Status::failure("too large"); + Status readFile(const std::string& path, int32_t maxBytes, + std::string& content) const override { + const std::map::const_iterator found = + files.find(path); + if (found == files.end()) + return Status::failure("no such file"); + if (static_cast(found->second.size()) > maxBytes) + return Status::failure("too large"); content = found->second; return Status::success(); } - Status readLineAt(const std::string& path, int32_t offset, int32_t, bool& found, std::string& line, + Status readLineAt(const std::string& path, int32_t offset, int32_t, + bool& found, std::string& line, int32_t& nextOffset) const override { - const std::map::const_iterator file = files.find(path); - if (file == files.end()) return Status::failure("no such file"); + const std::map::const_iterator file = + files.find(path); + if (file == files.end()) + return Status::failure("no such file"); if (offset >= static_cast(file->second.size())) { found = false; return Status::success(); } const size_t end = file->second.find('\n', offset); - line = file->second.substr(offset, end == std::string::npos ? std::string::npos : end - offset); - nextOffset = end == std::string::npos ? static_cast(file->second.size()) : static_cast(end + 1); + line = file->second.substr( + offset, end == std::string::npos ? std::string::npos : end - offset); + nextOffset = end == std::string::npos + ? static_cast(file->second.size()) + : static_cast(end + 1); found = true; return Status::success(); } @@ -113,8 +134,11 @@ struct Fs : FsProvider { return Status::success(); } Status removeTree(const std::string&) override { return Status::success(); } - Status rename(const std::string&, const std::string&) override { return Status::success(); } - Status writeFile(const std::string& path, const std::string& content) override { + Status rename(const std::string&, const std::string&) override { + return Status::success(); + } + Status writeFile(const std::string& path, + const std::string& content) override { files[path] = content; written = content; return Status::success(); @@ -135,30 +159,47 @@ struct Gui : GuiProvider { int32_t height() const override { return 240; } int32_t rotation() const override { return degrees; } void setRotation(int32_t value) override { degrees = value; } - int32_t color(int32_t r, int32_t g, int32_t b) const override { return (r << 16) | (g << 8) | b; } + int32_t color(int32_t r, int32_t g, int32_t b) const override { + return (r << 16) | (g << 8) | b; + } void clear(int32_t) override { trace += "clear;"; } - void fillRect(int32_t, int32_t, int32_t, int32_t, int32_t) override { trace += "fillRect;"; } - void drawRect(int32_t, int32_t, int32_t, int32_t, int32_t) override { trace += "drawRect;"; } - void drawLine(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t) override { trace += "drawLine;"; } + void fillRect(int32_t, int32_t, int32_t, int32_t, int32_t) override { + trace += "fillRect;"; + } + void drawRect(int32_t, int32_t, int32_t, int32_t, int32_t) override { + trace += "drawRect;"; + } + void drawLine(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t) override { + trace += "drawLine;"; + } void drawPixel(int32_t, int32_t, int32_t) override { trace += "drawPixel;"; } - void drawCircle(int32_t, int32_t, int32_t, int32_t, int32_t) override { trace += "drawCircle;"; } - void fillCircle(int32_t, int32_t, int32_t, int32_t, const int32_t*) override { trace += "fillCircle;"; } - void roundRect(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, const int32_t* top, const int32_t* bottom, + void drawCircle(int32_t, int32_t, int32_t, int32_t, int32_t) override { + trace += "drawCircle;"; + } + void fillCircle(int32_t, int32_t, int32_t, int32_t, const int32_t*) override { + trace += "fillCircle;"; + } + void roundRect(int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, + const int32_t* top, const int32_t* bottom, const int32_t* border) override { - if (top && bottom && *top != *bottom) gradient = true; + if (top && bottom && *top != *bottom) + gradient = true; trace += "roundRect("; trace += top ? "fill" : "-"; 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. + // Stands in for an e-ink panel, where a second commit is a second visible + // refresh. void commit() override { commits++; } int commits = 0; - void fillPolygon(const int32_t*, const int32_t*, size_t count, int32_t) override { + void fillPolygon(const int32_t*, const int32_t*, size_t count, + int32_t) override { trace += "fillPolygon" + std::to_string(count) + ";"; } - Status drawBmp(const std::string&, const int32_t*, const int32_t*, const int32_t*, const int32_t*) override { + Status drawBmp(const std::string&, const int32_t*, const int32_t*, + const int32_t*, const int32_t*) override { trace += "drawBmp;"; return Status::success(); } @@ -166,7 +207,8 @@ struct Gui : GuiProvider { return static_cast(text.size()) * 8; } int32_t fontHeight(int32_t, int32_t) const override { return 16; } - void drawText(int32_t, int32_t, int32_t, const std::string& text, int32_t, int32_t, const int32_t*) override { + void drawText(int32_t, int32_t, int32_t, const std::string& text, int32_t, + int32_t, const int32_t*) override { trace += "drawText(" + text + ");"; } }; @@ -177,8 +219,10 @@ struct Http : HttpProvider { std::vector headers; int32_t limit = 0; - Status request(const std::string& nextMethod, const std::string&, const std::string& nextBody, - const std::vector& nextHeaders, int32_t maxBytes, HttpResponse& response) override { + Status request(const std::string& nextMethod, const std::string&, + const std::string& nextBody, + const std::vector& nextHeaders, int32_t maxBytes, + HttpResponse& response) override { method = nextMethod; body = nextBody; headers = nextHeaders; @@ -187,7 +231,8 @@ struct Http : HttpProvider { response.body = "missing"; return Status::success(); } - Status download(const std::string&, const std::string&, const HttpDownload& options, int32_t& bytesWritten) override { + Status download(const std::string&, const std::string&, + const HttpDownload& options, int32_t& bytesWritten) override { bytesWritten = options.maxBytes; return Status::success(); } @@ -242,15 +287,19 @@ struct Ble : BleProvider { Status connect(const std::string&) override { return Status::success(); } void disconnect() override {} bool isConnected() const override { return true; } - Status read(const std::string&, const std::string&, std::string& out) override { + Status read(const std::string&, const std::string&, + std::string& out) override { out = std::string("a\0b", 3); return Status::success(); } - Status write(const std::string&, const std::string&, const std::string& next) override { + Status write(const std::string&, const std::string&, + const std::string& next) override { value = next; return Status::success(); } - Status startAdvertising(const std::string*) override { return Status::success(); } + Status startAdvertising(const std::string*) override { + return Status::success(); + } void stopAdvertising() override {} }; @@ -269,7 +318,8 @@ struct Touch : TouchProvider { return down; } bool isTouched() const override { return down; } - Status setCalibration(int32_t x0, int32_t y0, int32_t x1, int32_t y1) override { + Status setCalibration(int32_t x0, int32_t y0, int32_t x1, + int32_t y1) override { calibration[0] = x0; calibration[1] = y0; calibration[2] = x1; @@ -288,7 +338,9 @@ struct Buttons : ButtonsProvider { return roles; } bool isAnyPressed() const override { return false; } - bool isPressed(const std::string& button) const override { return button == "confirm"; } + bool isPressed(const std::string& button) const override { + return button == "confirm"; + } bool wasPressed(const std::string&) const override { return false; } bool wasReleased(const std::string&) const override { return false; } }; @@ -324,4 +376,4 @@ struct Bench { } }; -} // namespace fake +} // namespace fake diff --git a/native/test/runtime_test.cpp b/native/test/runtime_test.cpp index 9a70b7e..21594cd 100644 --- a/native/test/runtime_test.cpp +++ b/native/test/runtime_test.cpp @@ -25,7 +25,7 @@ void expectError(lua_State* state, const char* chunk) { lua_pop(state, 1); } -} // namespace +} // namespace int main() { fake::Bench bench; @@ -37,88 +37,82 @@ 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'))"); + 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.getAPIVersion() == 1)\n" - "assert(sys.hasFeature('touch') and sys.hasFeature('buttons'))\n" - "assert(not sys.hasFeature('eink'))\n" - "local free, total, largest = sys.getMemory()\n" - "assert(free == 100 and total == 200 and largest == 50)"); + run(state, "assert(sys.getAPIVersion() == 1)\n" + "assert(sys.hasFeature('touch') and sys.hasFeature('buttons'))\n" + "assert(not sys.hasFeature('eink'))\n" + "local free, total, largest = sys.getMemory()\n" + "assert(free == 100 and total == 200 and largest == 50)"); bench.fs.files["/notes.txt"] = "first\nsecond"; - run(state, - "assert(fs.MAX_READ_BYTES == 65536)\n" - "assert(fs.exists('/notes.txt') and not fs.exists('/missing'))\n" - "assert(fs.fileSize('/notes.txt') == 12)\n" - "local size, err = fs.fileSize('/missing')\n" - "assert(size == nil and err == 'no such file')\n" - "assert(fs.readFile('/notes.txt', 64) == 'first\\nsecond')\n" - "local line, nextOffset = fs.readLineAt('/notes.txt', 0, 64)\n" - "assert(line == 'first' and nextOffset == 6)\n" - "assert(fs.readLineAt('/notes.txt', 12, 64) == nil)\n" - "assert(fs.listFiles('/')[1] == 'main.lua')\n" - "assert(fs.writeFile('/out.bin', 'a\\0b'))"); + run(state, "assert(fs.MAX_READ_BYTES == 65536)\n" + "assert(fs.exists('/notes.txt') and not fs.exists('/missing'))\n" + "assert(fs.fileSize('/notes.txt') == 12)\n" + "local size, err = fs.fileSize('/missing')\n" + "assert(size == nil and err == 'no such file')\n" + "assert(fs.readFile('/notes.txt', 64) == 'first\\nsecond')\n" + "local line, nextOffset = fs.readLineAt('/notes.txt', 0, 64)\n" + "assert(line == 'first' and nextOffset == 6)\n" + "assert(fs.readLineAt('/notes.txt', 12, 64) == nil)\n" + "assert(fs.listFiles('/')[1] == 'main.lua')\n" + "assert(fs.writeFile('/out.bin', 'a\\0b'))"); 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(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')"); 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)"); - run(state, - "local response = http.get('https://example.test', {maxBytes = 16, headers = {Accept = 'text/plain'}})\n" - "assert(response.status == 404 and response.body == 'missing')\n" - "assert(http.urlencode('a b/c~') == 'a%20b%2Fc~')\n" - "assert(http.download('https://example.test/f', '/f', {maxBytes = 32}) == 32)"); + run(state, "local response = http.get('https://example.test', {maxBytes = " + "16, headers = {Accept = 'text/plain'}})\n" + "assert(response.status == 404 and response.body == 'missing')\n" + "assert(http.urlencode('a b/c~') == 'a%20b%2Fc~')\n" + "assert(http.download('https://example.test/f', '/f', {maxBytes = " + "32}) == 32)"); assert(bench.http.method == "GET" && bench.http.limit == 16); - assert(bench.http.headers.size() == 1 && bench.http.headers[0].name == "Accept"); + assert(bench.http.headers.size() == 1 && + bench.http.headers[0].name == "Accept"); expectError(state, "http.get('https://example.test', {maxBytes = 999999})"); - run(state, - "assert(wifi.scan()[1].ssid == 'home')\n" - "assert(wifi.isConnected())\n" - "assert(wifi.getLocalIP() == '192.168.1.5')\n" - "assert(wifi.getStatus().state == 'connected')\n" - "assert(wifi.connect())"); + run(state, "assert(wifi.scan()[1].ssid == 'home')\n" + "assert(wifi.isConnected())\n" + "assert(wifi.getLocalIP() == '192.168.1.5')\n" + "assert(wifi.getStatus().state == 'connected')\n" + "assert(wifi.connect())"); assert(bench.wifi.savedReconnect); - run(state, - "assert(ble.scan()[1].address == 'aa:bb')\n" - "assert(#ble.read('svc', 'chr') == 3)\n" - "assert(ble.write('svc', 'chr', 'x\\0y'))"); + run(state, "assert(ble.scan()[1].address == 'aa:bb')\n" + "assert(#ble.read('svc', 'chr') == 3)\n" + "assert(ble.write('svc', 'chr', 'x\\0y'))"); assert(bench.ble.duration == 3000); assert(bench.ble.value.size() == 3); - run(state, - "fired = 0\n" - "once = timer.after(10, function() fired = fired + 1 end)\n" - "repeated = timer.every(5, function() fired = fired + 10 end)"); + run(state, "fired = 0\n" + "once = timer.after(10, function() fired = fired + 1 end)\n" + "repeated = timer.every(5, function() fired = fired + 10 end)"); assert(bench.timer.scheduled.size() == 2); runtime.callTimer(bench.timer.scheduled[0]); - runtime.callTimer(bench.timer.scheduled[0]); // a one-shot never fires twice + runtime.callTimer(bench.timer.scheduled[0]); // a one-shot never fires twice runtime.callTimer(bench.timer.scheduled[1]); runtime.callTimer(bench.timer.scheduled[1]); - run(state, - "assert(fired == 21)\n" - "assert(timer.cancel(repeated))\n" - "assert(not timer.cancel(repeated))"); + run(state, "assert(fired == 21)\n" + "assert(timer.cancel(repeated))\n" + "assert(not timer.cancel(repeated))"); assert(bench.timer.cancelled.size() == 1); expectError(state, "timer.after(0, function() end)"); @@ -128,24 +122,28 @@ int main() { "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] == 'confirm')"); + "assert(#input.getButtons() == 4 and input.getButtons()[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)"); + // 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); 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', pad = 4, gap = 2})\n" + "local root = node.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 = true})\n" - "node.setStyle(root, {background = 0xFFFFFF, fill = 0xFFFFFF, color = 0, face = 0xEEEEEE,\n" + "local button = node.create(root, {type = 'button', h = 40, interactive " + "= true})\n" + "node.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" @@ -169,17 +167,20 @@ int main() { run(state, "node.reset()\n" - "local root = node.create(nil, {type = 'custom', w = 'fill', h = 'fill'})\n" + "local root = node.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" "assert(painted == 320)"); - // Callbacks: only init failing stops an app, and a release fires the tap alias after the up. + // 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" + "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" @@ -191,20 +192,21 @@ int main() { 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::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. + // 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)"); + run(state, "function draw() timer.after(1, function() end) end\n" + "nested = timer.after(1, function() draw() end)"); bench.gui.commits = 0; runtime.callTimer(bench.timer.scheduled.back()); assert(bench.gui.commits == 1); @@ -213,10 +215,11 @@ int main() { 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 + runtime.callDraw(1); // a failed frame logs and the app keeps running assert(bench.log.message.find("draw: ") == 0); - // A feature callback without its provider is a wiring bug, not a silent no-op. + // A feature callback without its provider is a wiring bug, not a silent + // no-op. { fake::Bench headless; esp32lua::Providers providers = headless.providers(); @@ -225,17 +228,21 @@ 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(input.getTouch == nil and input.isPressed ~= nil)"); } - // App loading: a fresh state per app, require reaching the app directory and /.lua/lib, and - // navigation applied between batches rather than inside a callback. + // App loading: a fresh state per app, require reaching the app directory and + // /.lua/lib, and navigation applied between batches rather than inside a + // callback. { fake::Bench host; - host.fs.files["/.lua/lib/greet.lua"] = "return {hello = function() return 'hi' end}"; + 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"; + "function init(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"; @@ -251,7 +258,8 @@ int main() { // A subapp shares the app ID, so both routes share one data directory. run(app.state(), "sys.launch('Reader', 'book.epub')"); assert(app.hasPendingNavigation()); - run(app.state(), "assert(started == 'hi:')"); // the current app keeps running until applied + run(app.state(), "assert(started == 'hi:')"); // the current app keeps + // running until applied assert(app.applyPendingNavigation()); run(app.state(), "assert(started == 'page:book.epub')"); run(app.state(), "sys.launch('Reader/Notes')"); @@ -267,7 +275,8 @@ int main() { run(app.state(), "sys.back()"); assert(app.applyPendingNavigation() && app.appPath() == "Home"); - // sys.replace does not grow history, so back from it still reaches the launcher. + // sys.replace does not grow history, so back from it still reaches the + // launcher. run(app.state(), "sys.replace('Reader', 'other.epub')"); assert(app.applyPendingNavigation() && app.appPath() == "Reader"); run(app.state(), "sys.back()"); @@ -276,7 +285,8 @@ int main() { // A missing app, a broken app, and a traversal all leave nothing running. assert(!app.startApp("Absent")); assert(!app.hasApp() && app.state() == nullptr); - host.fs.files["/.lua/apps/Broken/main.lua"] = "function init() error('nope') end"; + host.fs.files["/.lua/apps/Broken/main.lua"] = + "function init() error('nope') end"; assert(!app.startApp("Broken")); assert(!app.hasApp()); assert(!app.startApp("../secrets")); diff --git a/tools/gen_api.py b/tools/gen_api.py index c0c8d66..60a1096 100644 --- a/tools/gen_api.py +++ b/tools/gen_api.py @@ -35,6 +35,7 @@ DOC = re.compile(r"^\s*// ---\s?(?P.*)$") PARAM = re.compile(r"^\s*// @param (?P\w+) (?P\S+)(?: (?P.*))?$") RETURN = re.compile(r"^\s*// @return (?P\S+)(?: (?P.*))?$") TAG = re.compile(r"^\s*// @(?P[\w-]+)") +CONTINUATION = re.compile(r"^\s*// (?P(?!@|---)\S.*)$") TYPES = {"int": "integer", "bool": "boolean"} @@ -65,6 +66,7 @@ def parse(path): current = None doc = {"doc": [], "params": [], "returns": []} in_table = False + continuation = None def reset(): return {"doc": [], "params": [], "returns": []} @@ -73,6 +75,18 @@ def parse(path): stripped = line.strip() where = f"{path.relative_to(ROOT)}:{number}" + wrapped = CONTINUATION.match(line) + if continuation and wrapped: + items, index, field = continuation + if field is None: + items[index] += " " + wrapped.group("text") + else: + item = list(items[index]) + item[field] += " " + wrapped.group("text") + items[index] = tuple(item) + continue + continuation = None + module = MODULE.match(stripped) if module: pending_module = new_module(module.group("kind"), module.group("name"), module.group("class_")) @@ -104,12 +118,15 @@ def parse(path): target["consts"].append( (const.group("name"), lua_type(const.group("type")), const.group("value"), const.group("desc") or "") ) + continuation = (target["consts"], len(target["consts"]) - 1, 3) continue fix = FIX.match(stripped) if fix: if not target: raise SystemExit(f"{where}: @lua-{fix.group('where')} outside a module") - target[fix.group("where")].append(fix.group("text")) + items = target[fix.group("where")] + items.append(fix.group("text")) + continuation = (items, len(items) - 1, None) continue table = TABLE.match(line) @@ -128,14 +145,17 @@ def parse(path): match = DOC.match(line) if match: doc["doc"].append(match.group("text").strip()) + continuation = (doc["doc"], len(doc["doc"]) - 1, None) continue match = PARAM.match(line) if match: doc["params"].append((match.group("name"), lua_type(match.group("type")), match.group("desc") or "")) + continuation = (doc["params"], len(doc["params"]) - 1, 2) continue match = RETURN.match(line) if match: doc["returns"].append((lua_type(match.group("type")), match.group("desc") or "")) + continuation = (doc["returns"], len(doc["returns"]) - 1, 1) continue match = TAG.match(line) if match: diff --git a/tools/test_gen_api.py b/tools/test_gen_api.py new file mode 100644 index 0000000..72c56ab --- /dev/null +++ b/tools/test_gen_api.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python3 + +import tempfile +from pathlib import Path + +from gen_api import ROOT, parse + + +with tempfile.TemporaryDirectory(dir=ROOT) as directory: + source = Path(directory) / "wrapped.cpp" + source.write_text( + """// @lua-module fs FsLib +// @lua-const MAX_READ_BYTES integer 65536 Largest portable +// whole-file read. +const luaL_Reg FUNCTIONS[] = { + // --- Reads one complete + // file. + // @param path string Absolute file + // path. + // @return string|nil File + // contents. + {"readFile", readFile}, +}; +""" + ) + module = parse(source)[0] + +assert module["consts"][0][3] == "Largest portable whole-file read." +doc = module["functions"][0][1] +assert doc["doc"] == ["Reads one complete file."] +assert doc["params"][0][2] == "Absolute file path." +assert doc["returns"][0][1] == "File contents." +print("ok")