Files
slate32/scripts/gen_lua_stubs.py
evan 7c0928d84e feat(ui): add drag gestures and a scrollable box
The firmware now fires on_touch_move with a 2px noise threshold; gesture
intent is the toolkit's 8px slop, since a painting app's stroke starts at
the first real pixel. Past the slop, Screen:move hands the drag to the
nearest scroll ancestor and cancels the widget under the finger.

ui.scroll leans on the app viewport for clipping, so it must reach both
panel edges. Paint and Scroll demo both halves of the gesture split.
2026-08-02 16:59:42 -04:00

162 lines
5.8 KiB
Python

#!/usr/bin/env python3
"""Generate the LuaLS stub for the firmware's Lua API.
Adapted from crosspoint-reader's scripts/gen_lua_stubs.py. The emitted format is
deliberately identical so an editor configured for one repo works for the other; the
parser differs because this firmware registers bindings as annotated luaL_Reg tables
rather than addFunction() calls, which keeps a module's documentation contiguous.
python3 scripts/gen_lua_stubs.py # write the stub
python3 scripts/gen_lua_stubs.py --check # exit 1 if it is stale
"""
import re
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
SOURCES = sorted((ROOT / "src/lua/bindings").glob("*.cpp"))
OUTPUT = ROOT / "stubs/slate32.lua"
TYPES = {"int": "integer", "bool": "boolean"}
ENTRY = re.compile(r'^\s*\{"(?P<name>\w+)",\s*\w+\}')
TABLE = re.compile(r"static const luaL_Reg (?P<var>\w+)\[\]")
BIND = re.compile(r'luaL_newlib\(L,\s*(?P<var>\w+)\);\s*\n\s*lua_setglobal\(L,\s*"(?P<global>\w+)"\)')
DOC = re.compile(r"^\s*//\s*---\s?(?P<text>.*)$")
PARAM = re.compile(r"^\s*//\s*@param\s+(?P<name>\w+)\s+(?P<type>\S+)(?:\s+(?P<desc>.*))?$")
RETURN = re.compile(r"^\s*//\s*@return\s+(?P<type>\S+)(?:\s+(?P<desc>.*))?$")
TAG = re.compile(r"^\s*//\s*@(?P<tag>\w+)")
def lua_type(raw):
"""`int|nil` becomes `integer?`, matching how LuaLS spells optionality."""
optional = raw.endswith("|nil")
if optional:
raw = raw[: -len("|nil")]
mapped = TYPES.get(raw, raw)
return f"{mapped}?" if optional else mapped
def parse(path):
"""Collect {var: [function, ...]} and {var: global name} from one file."""
text = path.read_text()
tables, bindings = {}, {}
for match in BIND.finditer(text):
bindings[match.group("var")] = match.group("global")
lines = text.splitlines()
var, pending = None, {"doc": [], "params": [], "returns": []}
def reset():
return {"doc": [], "params": [], "returns": []}
for line in lines:
table = TABLE.search(line)
if table:
var = table.group("var")
tables.setdefault(var, [])
pending = reset()
continue
if var is None:
continue
doc = DOC.match(line)
if doc:
pending["doc"].append(doc.group("text").strip())
continue
param = PARAM.match(line)
if param:
pending["params"].append(
(param.group("name"), lua_type(param.group("type")), (param.group("desc") or "").strip())
)
continue
ret = RETURN.match(line)
if ret:
pending["returns"].append((lua_type(ret.group("type")), (ret.group("desc") or "").strip()))
continue
tag = TAG.match(line)
if tag:
raise SystemExit(f"{path.name}: unknown tag @{tag.group('tag')}")
entry = ENTRY.match(line)
if entry:
name = entry.group("name")
if not pending["doc"]:
raise SystemExit(f"{path.name}: {name} is registered without a description")
tables[var].append((name, pending))
pending = reset()
continue
if "};" in line:
var = None
return tables, bindings
def render(modules):
out = [
"---@meta",
"",
"-- Generated by scripts/gen_lua_stubs.py. Do not edit.",
"-- Point your editor's Lua language server at this file to get completion for the",
"-- firmware API inside sdcard/apps and sdcard/lib.",
"",
]
for global_name in sorted(modules):
functions = modules[global_name]
out.append(f"---@class {global_name}lib")
out.append(f"{global_name} = {{}}")
out.append("")
for name, meta in functions:
for line in meta["doc"]:
out.append(f"--- {line}" if line else "---")
for param, type_, desc in meta["params"]:
out.append(f"---@param {param} {type_}{(' ' + desc) if desc else ''}")
for type_, desc in meta["returns"]:
out.append(f"---@return {type_}{(' ' + desc) if desc else ''}")
args = ", ".join(param for param, _, _ in meta["params"])
out.append(f"function {global_name}.{name}({args}) end")
out.append("")
out.extend(
[
"-- Callbacks an app may define as globals:",
"-- init() once, before the first draw (required)",
"-- draw() every 33 ms while the app runs",
"-- on_tick() at the interval app.setTickInterval() asked for",
"-- on_touch_down(x, y) finger down",
"-- on_touch_move(x, y) finger moved while down",
"-- on_touch_up(x, y) finger up",
"-- on_touch(x, y) tap, fired on release like a click",
"",
]
)
return "\n".join(out)
def main():
modules = {}
for path in SOURCES:
tables, bindings = parse(path)
for var, functions in tables.items():
if var not in bindings:
continue # a table that is never installed as a global documents nothing
modules.setdefault(bindings[var], []).extend(functions)
if not modules:
raise SystemExit("no annotated bindings found")
rendered = render(modules)
if "--check" in sys.argv:
current = OUTPUT.read_text() if OUTPUT.exists() else ""
if current != rendered:
raise SystemExit(f"{OUTPUT.relative_to(ROOT)} is stale; run scripts/gen_lua_stubs.py")
return
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
OUTPUT.write_text(rendered)
print(f"wrote {OUTPUT.relative_to(ROOT)}: {sum(len(f) for f in modules.values())} functions")
if __name__ == "__main__":
main()