diff options
| author | t <t@tjp.lol> | 2026-06-02 19:41:56 -0600 |
|---|---|---|
| committer | t <t@tjp.lol> | 2026-06-02 22:09:31 -0600 |
| commit | 442888c6c306cfff0708c3d4bbbeda685cec2e75 (patch) | |
| tree | 858ad09d4da908cc7e61ca5e570e2f46e74bdb68 /src/command.zig | |
| parent | 8b88b886346460b1ab29edb03ad85bc3bb565a45 (diff) | |
lua /commands
Diffstat (limited to 'src/command.zig')
| -rw-r--r-- | src/command.zig | 86 |
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(); |
