summaryrefslogtreecommitdiff
path: root/src/lua_runtime.zig
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-09 00:00:56 -0600
committert <t@tjp.lol>2026-06-09 08:52:47 -0600
commit97b10466d83d488ad2cb9e084159a445af74c845 (patch)
tree6bea7c23d00ff1178a43e631fc700f210cb7d85b /src/lua_runtime.zig
parent104001d25c9c8cb5ec45ced1678f7c7b70888808 (diff)
event lifecycle
Diffstat (limited to 'src/lua_runtime.zig')
-rw-r--r--src/lua_runtime.zig124
1 files changed, 97 insertions, 27 deletions
diff --git a/src/lua_runtime.zig b/src/lua_runtime.zig
index eb760f8..797b97b 100644
--- a/src/lua_runtime.zig
+++ b/src/lua_runtime.zig
@@ -29,9 +29,12 @@ const std = @import("std");
const Allocator = std.mem.Allocator;
const panto = @import("panto");
const lua_bridge = @import("lua_bridge.zig");
+const lua_event_bridge = @import("lua_event_bridge.zig");
+const ui_event = @import("tui_event.zig");
const c = lua_bridge.c;
const Io = std.Io;
+const EventBridge = lua_event_bridge.EventBridge;
pub const SOURCE_NAME = "panto-lua";
@@ -67,7 +70,7 @@ pub const LuaRuntime = struct {
/// the Lua registry (`luaL_ref` index).
handlers: std.StringHashMap(c_int),
- /// Slash commands declared by extensions via `panto.register_command`.
+ /// Slash commands declared by extensions via `panto.ext.register_command`.
/// Owned by this runtime; the CLI reads these to build its command
/// registry. Each entry's name/description point into `strings`.
commands: std.array_list.Managed(LuaCommand),
@@ -92,8 +95,14 @@ pub const LuaRuntime = struct {
/// thread per source per turn).
current_batch: ?*BatchState = null,
+ /// The extension UI event bridge (plan ยง7.6): owns Lua `on` handlers
+ /// and Lua-defined components, and drives the App's native `EventBus`.
+ /// Heap-allocated so its address is stable (the bridge stores `*self`
+ /// pointers in Lua light-userdata upvalues and handler ctx).
+ event_bridge: *EventBridge,
+
/// Create a new runtime. The `lua_State` is opened, standard libs
- /// loaded, and the `panto.register_tool` bridge installed.
+ /// loaded, and the `panto.ext.register_tool` bridge installed.
pub fn create(allocator: Allocator) !*LuaRuntime {
const self = try allocator.create(LuaRuntime);
errdefer allocator.destroy(self);
@@ -103,6 +112,10 @@ pub const LuaRuntime = struct {
c.luaL_openlibs(L);
lua_bridge.install(L);
+ const eb = try allocator.create(EventBridge);
+ errdefer allocator.destroy(eb);
+ eb.* = EventBridge.init(allocator, L);
+
self.* = .{
.allocator = allocator,
.L = L,
@@ -111,10 +124,21 @@ pub const LuaRuntime = struct {
.handlers = std.StringHashMap(c_int).init(allocator),
.commands = std.array_list.Managed(LuaCommand).init(allocator),
.command_handlers = std.StringHashMap(c_int).init(allocator),
+ .event_bridge = eb,
};
+
+ // Install the real `panto.ext.emit` now that the bridge exists, so
+ // a Lua `emit(name, data)` reaches the native bus once attached.
+ lua_bridge.installEmit(L, @ptrCast(eb), lua_event_bridge.emitThunk);
return self;
}
+ /// The extension UI event bridge. The embedder wires its `attachBus`
+ /// to the App's `EventBus` during startup (after the App exists).
+ pub fn eventBridge(self: *LuaRuntime) *EventBridge {
+ return self.event_bridge;
+ }
+
/// Install the libuv-driven coroutine scheduler:
/// - Register `panto._record_result` (C function with `self` as
/// light-userdata upvalue) so the wrapper closure can hand
@@ -154,6 +178,11 @@ pub const LuaRuntime = struct {
c.luaL_unref(self.L, lua_bridge.LUA_REGISTRYINDEX, self.uv_run_ref);
}
+ // Free the UI event bridge (Lua handler/component refs + caches)
+ // BEFORE closing the state, since it unrefs registry entries.
+ self.event_bridge.deinit();
+ self.allocator.destroy(self.event_bridge);
+
c.lua_close(self.L);
self.decls.deinit();
@@ -168,7 +197,7 @@ pub const LuaRuntime = struct {
/// `package_root`, if provided, is prepended to `package.path` so
/// `require` finds sibling modules.
///
- /// All `panto.register_tool` calls in the script run during this
+ /// All `panto.ext.register_tool` calls in the script run during this
/// call. The runtime then harvests the registrations table,
/// transfers handler functions into the Lua registry (one `luaL_ref`
/// per tool), and records each tool's metadata in `self.decls`.
@@ -204,10 +233,10 @@ pub const LuaRuntime = struct {
}
/// Load a single-tool Lua script and register the table it returns
- /// as if `panto.register_tool` had been called on that table.
+ /// as if `panto.ext.register_tool` had been called on that table.
///
/// The script's top-level chunk must return a table with the same
- /// shape that `panto.register_tool` accepts:
+ /// shape that `panto.ext.register_tool` accepts:
/// `{ name, description, schema, handler }`. This is the ergonomic
/// form supported under a `tools/` directory.
pub fn loadTool(
@@ -230,14 +259,17 @@ pub const LuaRuntime = struct {
// table). Use luaL_loadfilex + lua_pcallk directly so we can
// ask for a return value (the bridge's loadFile discards them).
//
- // Push `panto.register_tool` *first*, then load+run the chunk so
- // its return value naturally lands above it; calling pcall then
+ // Push `panto.ext.register_tool` *first*, then load+run the chunk
+ // so its return value naturally lands above it; calling pcall then
// consumes both in the right order.
const L = self.L;
_ = c.lua_getglobal(L, "panto");
+ _ = c.lua_getfield(L, -1, "ext");
_ = c.lua_getfield(L, -1, "register_tool");
- c.lua_copy(L, -1, -2); // overwrite `panto` with `register_tool`
- c.lua_settop(L, c.lua_gettop(L) - 1); // pop the duplicate
+ // Stack: ..., panto, ext, register_tool. Collapse to just
+ // register_tool by overwriting the lowest of the three.
+ c.lua_copy(L, -1, -3); // overwrite `panto` with `register_tool`
+ c.lua_settop(L, c.lua_gettop(L) - 2); // pop ext + duplicate
// Stack: ..., register_tool
if (c.luaL_loadfilex(L, path_z.ptr, null) != 0) {
@@ -262,7 +294,7 @@ pub const LuaRuntime = struct {
// Invoke register_tool(returned_table). Same validation, schema
// serialization, and registrations-table append logic as an
- // extension's `panto.register_tool` call.
+ // extension's `panto.ext.register_tool` call.
if (c.lua_pcallk(L, 1, 0, 0, 0, null) != 0) {
logTopAsError(L, "lua: register_tool failed for tool script");
return error.LuaRunFailed;
@@ -336,6 +368,44 @@ pub const LuaRuntime = struct {
}
try self.harvestAndStoreCommands();
+ try self.harvestAndStoreOnHandlers();
+ }
+
+ /// Walk the `panto.ext.on` registrations table populated by the script
+ /// just loaded. For each `{ event, handler }` record, hand the handler
+ /// function and event name to the `EventBridge`, which `luaL_ref`s the
+ /// function and remembers the subscription (in registration order).
+ /// Actual registration into the App's `EventBus` happens later via
+ /// `EventBridge.attachBus` (the bus may not exist at load time).
+ fn harvestAndStoreOnHandlers(self: *LuaRuntime) !void {
+ const L = self.L;
+
+ _ = c.lua_rawgetp(L, lua_bridge.LUA_REGISTRYINDEX, &lua_bridge.on_registrations_key);
+ defer c.lua_settop(L, c.lua_gettop(L) - 1);
+
+ const n: usize = @intCast(c.lua_rawlen(L, -1));
+ var i: usize = 1;
+ while (i <= n) : (i += 1) {
+ _ = c.lua_rawgeti(L, -1, @intCast(i)); // push record
+ defer c.lua_settop(L, c.lua_gettop(L) - 1); // pop record
+
+ // Read the event name (borrowed; the bridge dupes it).
+ _ = c.lua_getfield(L, -1, "event");
+ var elen: usize = 0;
+ const eptr = c.lua_tolstring(L, -1, &elen);
+ const event_name = if (eptr != null) eptr[0..elen] else "";
+ c.lua_settop(L, c.lua_gettop(L) - 1); // pop event string
+
+ // Push the handler function and hand it to the bridge (which
+ // pushes a copy + luaL_refs it).
+ _ = c.lua_getfield(L, -1, "handler");
+ if (c.lua_type(L, -1) != lua_bridge.T_FUNCTION) {
+ c.lua_settop(L, c.lua_gettop(L) - 1); // pop non-function
+ return RuntimeError.LuaHandlerNotFound;
+ }
+ try self.event_bridge.registerOnHandler(event_name, -1);
+ c.lua_settop(L, c.lua_gettop(L) - 1); // pop handler
+ }
}
/// Walk the command registrations table populated by the script just
@@ -1094,7 +1164,7 @@ test "loadExtension records tool decls" {
defer tmp.cleanup();
const source =
- \\panto.register_tool {
+ \\panto.ext.register_tool {
\\ name = "greet", description = "Says hi.",
\\ schema = { type = "object", properties = { name = { type = "string" } } },
\\ handler = function(input) return "hi, " .. input.name end,
@@ -1117,7 +1187,7 @@ test "loadExtension records command decls" {
const source =
\\_G.last_args = nil
- \\panto.register_command {
+ \\panto.ext.register_command {
\\ name = "shout", description = "Uppercases its args.",
\\ handler = function(args) _G.last_args = args:upper() end,
\\}
@@ -1153,7 +1223,7 @@ test "runCommand surfaces a Lua error" {
defer tmp.cleanup();
const source =
- \\panto.register_command {
+ \\panto.ext.register_command {
\\ name = "boom", description = "crashes",
\\ handler = function(args) error("kaboom") end,
\\}
@@ -1180,8 +1250,8 @@ test "duplicate command name within runtime is rejected" {
defer tmp.cleanup();
const source =
- \\panto.register_command { name = "dup", description = "a", handler = function() end }
- \\panto.register_command { name = "dup", description = "b", handler = function() end }
+ \\panto.ext.register_command { name = "dup", description = "a", handler = function() end }
+ \\panto.ext.register_command { name = "dup", description = "b", handler = function() end }
;
const path = try writeTempScript(tmp.dir, "dup.lua", source);
defer testing.allocator.free(path);
@@ -1196,12 +1266,12 @@ test "invokeBatch runs each call through a coroutine and returns the result" {
defer tmp.cleanup();
const source =
- \\panto.register_tool {
+ \\panto.ext.register_tool {
\\ name = "echo", description = "echoes",
\\ schema = { type = "object", properties = { msg = { type = "string" } } },
\\ handler = function(input) return "got: " .. input.msg end,
\\}
- \\panto.register_tool {
+ \\panto.ext.register_tool {
\\ name = "shout", description = "shouts",
\\ schema = { type = "object", properties = { msg = { type = "string" } } },
\\ handler = function(input) return input.msg:upper() .. "!" end,
@@ -1242,7 +1312,7 @@ test "module-global state survives across calls in the same runtime" {
const source =
\\local count = 0
- \\panto.register_tool {
+ \\panto.ext.register_tool {
\\ name = "bump", description = "increment counter",
\\ schema = { type = "object" },
\\ handler = function(input)
@@ -1294,12 +1364,12 @@ test "handler crash: per-call error surfaces, sibling calls succeed" {
defer tmp.cleanup();
const source =
- \\panto.register_tool {
+ \\panto.ext.register_tool {
\\ name = "ok", description = "ok",
\\ schema = { type = "object" },
\\ handler = function(input) return "fine" end,
\\}
- \\panto.register_tool {
+ \\panto.ext.register_tool {
\\ name = "boom", description = "bad",
\\ schema = { type = "object" },
\\ handler = function(input) error("kaboom") end,
@@ -1359,7 +1429,7 @@ test "directory-style extension can require sibling modules" {
.sub_path = "ext/init.lua",
.data =
\\local util = require("util")
- \\panto.register_tool {
+ \\panto.ext.register_tool {
\\ name = "shout", description = "uppercase + bang",
\\ schema = { type = "object", properties = { text = { type = "string" } } },
\\ handler = function(input) return util.shout(input.text) end,
@@ -1393,7 +1463,7 @@ test "yielding handler with no event loop surfaces LuaHandlerYielded" {
defer tmp.cleanup();
const source =
- \\panto.register_tool {
+ \\panto.ext.register_tool {
\\ name = "sleeper", description = "yields forever",
\\ schema = { type = "object" },
\\ handler = function(input) coroutine.yield() ; return "never" end,
@@ -1444,7 +1514,7 @@ test "scheduler: yielding handler is resumed by libuv" {
const source =
\\local uv = require("luv")
- \\panto.register_tool {
+ \\panto.ext.register_tool {
\\ name = "timer_say", description = "sleep then return",
\\ schema = { type = "object" },
\\ handler = function(input)
@@ -1521,12 +1591,12 @@ test "scheduler: handler that yields without arming libuv work is surfaced as an
var tmp = testing.tmpDir(.{});
defer tmp.cleanup();
const source =
- \\panto.register_tool {
+ \\panto.ext.register_tool {
\\ name = "abandon", description = "yields into the void",
\\ schema = { type = "object" },
\\ handler = function() coroutine.yield(); return "never" end,
\\}
- \\panto.register_tool {
+ \\panto.ext.register_tool {
\\ name = "fine", description = "sync ok",
\\ schema = { type = "object" },
\\ handler = function() return "ok" end,
@@ -1672,14 +1742,14 @@ test "loadExtension: duplicate tool name from a second extension errors" {
defer tmp.cleanup();
const a =
- \\panto.register_tool {
+ \\panto.ext.register_tool {
\\ name = "clash", description = "a",
\\ schema = { type = "object" },
\\ handler = function(input) return "a" end,
\\}
;
const b =
- \\panto.register_tool {
+ \\panto.ext.register_tool {
\\ name = "clash", description = "b",
\\ schema = { type = "object" },
\\ handler = function(input) return "b" end,