summaryrefslogtreecommitdiff
path: root/src/command.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/command.zig
parent8b88b886346460b1ab29edb03ad85bc3bb565a45 (diff)
lua /commands
Diffstat (limited to 'src/command.zig')
-rw-r--r--src/command.zig86
1 files changed, 84 insertions, 2 deletions
diff --git a/src/command.zig b/src/command.zig
index f10592f..26acc7a 100644
--- a/src/command.zig
+++ b/src/command.zig
@@ -17,8 +17,11 @@
//! model regardless.
//!
//! Builtin commands (e.g. `/compact`) register themselves from their own
-//! modules; future Lua extensions will append commands here too via a
-//! `panto.register_command` bridge.
+//! modules. Lua extensions append commands here too: a script's call to
+//! `panto.register_command { name, description, handler }` is harvested by
+//! `lua_runtime.zig`, and `main.zig` registers each via `registerLua`.
+//! Such commands carry a `lua_ref`; `dispatch` routes them back into the
+//! Lua runtime instead of calling a native `run` function.
const std = @import("std");
const panto = @import("panto");
@@ -46,6 +49,12 @@ pub const Context = struct {
/// Banner identity strings used when stamping persisted entries.
provider_name: []const u8,
model_name: []const u8,
+
+ /// The long-lived Lua runtime, if extensions are active. Used by
+ /// Lua-defined commands (see `lua_command.zig`) to invoke their
+ /// handler. `null` when no extensions loaded; Lua commands are only
+ /// registered when it is non-null, so handlers may assume it is set.
+ lua_rt: ?*anyopaque = null,
};
/// A single slash command.
@@ -57,6 +66,11 @@ pub const Command = struct {
/// Handler. `args` is the remainder of the line after the command
/// name, trimmed of surrounding whitespace (empty string if none).
run: *const fn (args: []const u8, ctx: *Context) anyerror!void,
+ /// For commands backed by a Lua handler, the `luaL_ref` of that
+ /// handler in the runtime's registry. `null` for native Zig
+ /// commands. The shared Lua thunk reads this to know which handler
+ /// to invoke (via `ctx.lua_rt`).
+ lua_ref: ?c_int = null,
};
pub const Error = error{
@@ -64,6 +78,13 @@ pub const Error = error{
CommandNotFound,
};
+/// Placeholder `run` for Lua-backed commands. Dispatch intercepts them
+/// via `Command.lua_ref` before `run` is ever called, so reaching here
+/// indicates a wiring bug.
+fn luaPlaceholderRun(_: []const u8, _: *Context) anyerror!void {
+ return error.LuaCommandMisrouted;
+}
+
/// A registry of slash commands. Owns its backing list; call `deinit`.
pub const Registry = struct {
allocator: std.mem.Allocator,
@@ -84,6 +105,23 @@ pub const Registry = struct {
try self.commands.append(self.allocator, command);
}
+ /// Register a Lua-backed command. `lua_ref` is the handler's registry
+ /// ref in the active `LuaRuntime`; dispatch routes to it via
+ /// `ctx.lua_rt`. The `run` field is a never-called placeholder.
+ pub fn registerLua(
+ self: *Registry,
+ name: []const u8,
+ description: []const u8,
+ lua_ref: c_int,
+ ) !void {
+ try self.register(.{
+ .name = name,
+ .description = description,
+ .run = luaPlaceholderRun,
+ .lua_ref = lua_ref,
+ });
+ }
+
/// Find a command by name (without leading `/`). Null if absent.
pub fn find(self: *const Registry, name: []const u8) ?*const Command {
for (self.commands.items) |*cmd| {
@@ -114,10 +152,38 @@ pub const Registry = struct {
const args = std.mem.trim(u8, body[name_end..], " \t");
const cmd = self.find(name) orelse return Error.CommandNotFound;
+ if (cmd.lua_ref) |ref| {
+ try runLuaCommand(ref, args, ctx);
+ return;
+ }
try cmd.run(args, ctx);
}
};
+/// Invoke a Lua-backed command. Resolves `ctx.lua_rt` back to a
+/// `LuaRuntime`, runs the handler synchronously, and prints any Lua
+/// error to the REPL writer (the handler itself is responsible for
+/// normal output). Kept out of `lua_runtime.zig` so that module stays
+/// free of REPL/`Context` concerns; the runtime only exposes
+/// `runCommand`.
+fn runLuaCommand(ref: c_int, args: []const u8, ctx: *Context) !void {
+ const lua_runtime = @import("lua_runtime.zig");
+ const rt: *lua_runtime.LuaRuntime = @ptrCast(@alignCast(ctx.lua_rt.?));
+
+ var err_buf: [4096]u8 = undefined;
+ var err_len: usize = 0;
+ rt.runCommand(ref, args, &err_buf, &err_len) catch |err| switch (err) {
+ error.LuaHandlerCrashed => {
+ try ctx.stdout.print(
+ "\n[command error: {s}]\n> ",
+ .{err_buf[0..err_len]},
+ );
+ try ctx.stdout_file.flush();
+ },
+ else => return err,
+ };
+}
+
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
@@ -151,6 +217,22 @@ test "register, find, list" {
}));
}
+test "registerLua records name, description, and ref; collides with builtins" {
+ var reg = Registry.init(testing.allocator);
+ defer reg.deinit();
+
+ try reg.register(.{ .name = "compact", .description = "d", .run = testHandler });
+ try reg.registerLua("hello", "Lua greeting", 7);
+
+ const cmd = reg.find("hello").?;
+ try testing.expectEqualStrings("Lua greeting", cmd.description);
+ try testing.expectEqual(@as(?c_int, 7), cmd.lua_ref);
+ // Native command has no lua_ref.
+ try testing.expectEqual(@as(?c_int, null), reg.find("compact").?.lua_ref);
+ // Name collision with a builtin is rejected.
+ try testing.expectError(error.DuplicateCommand, reg.registerLua("compact", "x", 1));
+}
+
test "dispatch splits name and args" {
var reg = Registry.init(testing.allocator);
defer reg.deinit();