summaryrefslogtreecommitdiff
path: root/src/lua_bridge.zig
diff options
context:
space:
mode:
authort <t@tjp.lol>2026-06-02 19:41:56 -0600
committert <t@tjp.lol>2026-06-02 22:09:31 -0600
commit442888c6c306cfff0708c3d4bbbeda685cec2e75 (patch)
tree858ad09d4da908cc7e61ca5e570e2f46e74bdb68 /src/lua_bridge.zig
parent8b88b886346460b1ab29edb03ad85bc3bb565a45 (diff)
lua /commands
Diffstat (limited to 'src/lua_bridge.zig')
-rw-r--r--src/lua_bridge.zig162
1 files changed, 159 insertions, 3 deletions
diff --git a/src/lua_bridge.zig b/src/lua_bridge.zig
index eca5661..cb57ade 100644
--- a/src/lua_bridge.zig
+++ b/src/lua_bridge.zig
@@ -77,6 +77,12 @@ pub const BridgeError = error{
/// needs to harvest entries.
pub var registrations_key: u8 = 0;
+/// The key under which we stash the *command* registrations table in
+/// `LUA_REGISTRYINDEX`. Distinct from `registrations_key` so tool and
+/// slash-command registrations never collide. Holds an array of records
+/// shaped `{ name=, description=, handler= }`.
+pub var command_registrations_key: u8 = 0;
+
/// A single declared tool, as harvested from a script's top-level call to
/// `panto.register_tool`. All slices reference Lua-owned strings on the
/// state's stack/registry; copy them before closing the state.
@@ -87,6 +93,14 @@ pub const Registration = struct {
schema_json: []const u8,
};
+/// A single declared slash command, harvested from a script's top-level
+/// call to `panto.register_command`. Slices reference Lua-owned strings;
+/// copy them before closing the state.
+pub const CommandRegistration = struct {
+ name: []const u8,
+ description: []const u8,
+};
+
// ---------------------------------------------------------------------------
// Public bridge API
// ---------------------------------------------------------------------------
@@ -102,10 +116,17 @@ pub fn install(L: *c.lua_State) void {
// Stash under our registry key.
c.lua_rawsetp(L, LUA_REGISTRYINDEX, &registrations_key);
- // Build the `panto` global table with `register_tool`.
- c.lua_createtable(L, 0, 1);
+ // Create the command registrations table (parallel to the tool one).
+ c.lua_createtable(L, 0, 0);
+ c.lua_rawsetp(L, LUA_REGISTRYINDEX, &command_registrations_key);
+
+ // Build the `panto` global table with `register_tool` and
+ // `register_command`.
+ c.lua_createtable(L, 0, 2);
c.lua_pushcclosure(L, registerToolThunk, 0);
c.lua_setfield(L, -2, "register_tool");
+ c.lua_pushcclosure(L, registerCommandThunk, 0);
+ c.lua_setfield(L, -2, "register_command");
c.lua_setglobal(L, "panto");
}
@@ -116,6 +137,8 @@ pub fn install(L: *c.lua_State) void {
pub fn resetRegistrations(L: *c.lua_State) void {
c.lua_createtable(L, 0, 0);
c.lua_rawsetp(L, LUA_REGISTRYINDEX, &registrations_key);
+ c.lua_createtable(L, 0, 0);
+ c.lua_rawsetp(L, LUA_REGISTRYINDEX, &command_registrations_key);
}
/// Load and execute a Lua source file in the given state. The file's
@@ -164,6 +187,33 @@ pub fn harvestRegistrations(
return out;
}
+/// Walk the *command* registrations table and copy each entry's name and
+/// description into arena-owned bytes. The handler field is ignored;
+/// callers that need to invoke handlers `luaL_ref` them separately.
+pub fn harvestCommandRegistrations(
+ L: *c.lua_State,
+ arena: Allocator,
+) BridgeError![]CommandRegistration {
+ _ = c.lua_rawgetp(L, LUA_REGISTRYINDEX, &command_registrations_key);
+ defer c.lua_settop(L, c.lua_gettop(L) - 1);
+
+ const n: usize = @intCast(c.lua_rawlen(L, -1));
+ if (n == 0) return arena.alloc(CommandRegistration, 0) catch BridgeError.OutOfMemory;
+
+ var out = arena.alloc(CommandRegistration, n) catch return BridgeError.OutOfMemory;
+ var i: usize = 1;
+ while (i <= n) : (i += 1) {
+ _ = c.lua_rawgeti(L, -1, @intCast(i));
+ defer c.lua_settop(L, c.lua_gettop(L) - 1);
+
+ out[i - 1] = .{
+ .name = try readStringField(L, -1, "name", arena),
+ .description = try readStringField(L, -1, "description", arena),
+ };
+ }
+ return out;
+}
+
/// In an *invocation-mode* state (registrations table populated by re-
/// running the script), push the handler function for `tool_name` onto the
/// stack. Caller is responsible for popping it after use.
@@ -287,6 +337,47 @@ fn registerToolThunk(L_opt: ?*c.lua_State) callconv(.c) c_int {
return 0;
}
+/// Implementation of `panto.register_command { name=, description=, handler= }`.
+///
+/// Expects a single table argument with three named fields. The handler
+/// is a function `function(args) ... end` where `args` is the trimmed
+/// remainder of the slash-command line (a string, possibly empty). Its
+/// return value is ignored — commands act by side effect, like the
+/// builtin Zig commands. Appends a `{ name, description, handler }`
+/// record to the command registrations table.
+fn registerCommandThunk(L_opt: ?*c.lua_State) callconv(.c) c_int {
+ const L = L_opt.?;
+ c.luaL_checktype(L, 1, T_TABLE);
+
+ // Stack after these pulls:
+ // 1: args table (input)
+ // 2: name (string)
+ // 3: description (string)
+ // 4: handler (function)
+ expectFieldNamed(L, 1, "name", T_STRING, "register_command");
+ expectFieldNamed(L, 1, "description", T_STRING, "register_command");
+ expectFieldNamed(L, 1, "handler", T_FUNCTION, "register_command");
+
+ // Build the record table { name, description, handler }.
+ c.lua_createtable(L, 0, 3);
+ c.lua_pushvalue(L, 2);
+ c.lua_setfield(L, -2, "name");
+ c.lua_pushvalue(L, 3);
+ c.lua_setfield(L, -2, "description");
+ c.lua_pushvalue(L, 4); // handler
+ c.lua_setfield(L, -2, "handler");
+
+ // Append to the command registrations table.
+ _ = c.lua_rawgetp(L, LUA_REGISTRYINDEX, &command_registrations_key);
+ // Stack: ..., record, regs_table
+ const n: c_int = @intCast(c.lua_rawlen(L, -1));
+ c.lua_pushvalue(L, -2); // copy record above regs_table
+ c.lua_rawseti(L, -2, n + 1);
+ c.lua_settop(L, c.lua_gettop(L) - 1); // pop regs_table
+
+ return 0;
+}
+
/// Push `args_table[field_name]` onto the stack and assert it has the
/// expected type. Raises a Lua error if missing or wrong type.
fn expectField(
@@ -295,12 +386,25 @@ fn expectField(
comptime field_name: [:0]const u8,
expected_type: c_int,
) void {
+ expectFieldNamed(L, args_idx, field_name, expected_type, "register_tool");
+}
+
+/// Like `expectField`, but lets the caller name the bridge function in
+/// the error message (`register_tool` vs `register_command`).
+fn expectFieldNamed(
+ L: *c.lua_State,
+ args_idx: c_int,
+ comptime field_name: [:0]const u8,
+ expected_type: c_int,
+ comptime fn_name: [:0]const u8,
+) void {
_ = c.lua_getfield(L, args_idx, field_name.ptr);
const got = c.lua_type(L, -1);
if (got != expected_type) {
+ const fmt: [:0]const u8 = fn_name ++ ": field '%s' must be %s (got %s)";
_ = c.luaL_error(
L,
- "register_tool: field '%s' must be %s (got %s)",
+ fmt.ptr,
field_name.ptr,
c.lua_typename(L, expected_type),
c.lua_typename(L, got),
@@ -476,6 +580,58 @@ test "install creates panto.register_tool global" {
try std.testing.expectEqual(@as(c_int, T_TABLE), c.lua_type(L, -1));
_ = c.lua_getfield(L, -1, "register_tool");
try std.testing.expectEqual(@as(c_int, T_FUNCTION), c.lua_type(L, -1));
+ _ = c.lua_getglobal(L, "panto");
+ _ = c.lua_getfield(L, -1, "register_command");
+ try std.testing.expectEqual(@as(c_int, T_FUNCTION), c.lua_type(L, -1));
+}
+
+test "register_command records name and description" {
+ const L = c.luaL_newstate() orelse return error.LuaInitFailed;
+ defer c.lua_close(L);
+ c.luaL_openlibs(L);
+ install(L);
+
+ const script =
+ \\panto.register_command {
+ \\ name = "hello",
+ \\ description = "Greets the user.",
+ \\ handler = function(args) return "hi " .. args end,
+ \\}
+ ;
+ if (c.luaL_loadstring(L, script) != 0 or c.lua_pcallk(L, 0, 0, 0, 0, null) != 0) {
+ var len: usize = 0;
+ const msg = c.lua_tolstring(L, -1, &len);
+ std.debug.print("lua error: {s}\n", .{msg[0..len]});
+ return error.LuaScriptFailed;
+ }
+
+ var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator);
+ defer arena_state.deinit();
+ const cmds = try harvestCommandRegistrations(L, arena_state.allocator());
+
+ try std.testing.expectEqual(@as(usize, 1), cmds.len);
+ try std.testing.expectEqualStrings("hello", cmds[0].name);
+ try std.testing.expectEqualStrings("Greets the user.", cmds[0].description);
+}
+
+test "register_command rejects a non-function handler" {
+ const L = c.luaL_newstate() orelse return error.LuaInitFailed;
+ defer c.lua_close(L);
+ c.luaL_openlibs(L);
+ install(L);
+
+ const script =
+ \\panto.register_command {
+ \\ name = "bad", description = "d", handler = 42,
+ \\}
+ ;
+ const loaded = c.luaL_loadstring(L, script) == 0;
+ try std.testing.expect(loaded);
+ // Should raise a Lua error (non-zero pcall result).
+ try std.testing.expect(c.lua_pcallk(L, 0, 0, 0, 0, null) != 0);
+ var len: usize = 0;
+ const msg = c.lua_tolstring(L, -1, &len);
+ try std.testing.expect(std.mem.indexOf(u8, msg[0..len], "register_command") != null);
}
test "register_tool records name, description, schema_json" {