From 97b10466d83d488ad2cb9e084159a445af74c845 Mon Sep 17 00:00:00 2001 From: t Date: Tue, 9 Jun 2026 00:00:56 -0600 Subject: event lifecycle --- src/main.zig | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) (limited to 'src/main.zig') diff --git a/src/main.zig b/src/main.zig index 7905347..34a687a 100644 --- a/src/main.zig +++ b/src/main.zig @@ -2,6 +2,7 @@ const std = @import("std"); const panto = @import("panto"); const lua_bridge = @import("lua_bridge.zig"); const lua_runtime = @import("lua_runtime.zig"); +const lua_event_bridge = @import("lua_event_bridge.zig"); const extension_loader = @import("extension_loader.zig"); const panto_home = @import("panto_home.zig"); const luarocks_runtime = @import("luarocks_runtime.zig"); @@ -24,6 +25,7 @@ const tui_theme = @import("tui_theme.zig"); const tui_component = @import("tui_component.zig"); const tui_engine = @import("tui_engine.zig"); const tui_components = @import("tui_components.zig"); +const tui_event = @import("tui_event.zig"); const tui_app = @import("tui_app.zig"); // Shorthand alias for the Lua C API. The bridge module owns the actual @@ -43,6 +45,7 @@ test { std.testing.refAllDecls(@This()); _ = lua_bridge; _ = lua_runtime; + _ = lua_event_bridge; _ = extension_loader; _ = panto_home; _ = luarocks_runtime; @@ -61,6 +64,7 @@ test { _ = tui_component; _ = tui_engine; _ = tui_components; + _ = tui_event; _ = tui_app; } @@ -401,7 +405,7 @@ pub fn main(init: std.process.Init) !void { try command_compaction.register(&cmd_registry); // Append slash commands declared by Lua extensions via - // `panto.register_command`. A name collision with a builtin (or + // `panto.ext.register_command`. A name collision with a builtin (or // between two extensions) surfaces as `error.DuplicateCommand` and // aborts startup, matching the tool-name collision policy. for (rt.commandList()) |lua_cmd| { @@ -485,6 +489,38 @@ pub fn main(init: std.process.Init) !void { ); defer app.deinit(); + // Wire the Lua extension UI event bridge to the App's event bus: this + // registers every `panto.ext.on(...)` handler (harvested at extension + // load time) into the bus, in registration order, so extensions can + // wrap/replace built-in components and a Lua `panto.ext.emit(...)` can + // drive the same bus. With no Lua handlers this is a no-op. + try rt.eventBridge().attachBus(app.eventBus()); + + // Install the override-release hook so a Lua-backed override that is + // SUPERSEDED by a later mid-stream swap (e.g. a `tool_details` handler + // replacing the `tool (?)` default's prior override) has its luaL_ref + + // RenderCache freed. Without this, each swapped Lua component would leak + // for the life of the runtime. The hook recognizes a bridged component by + // its vtable identity and ignores native components (see + // `EventBridge.releaseOverride`). + // + // TEARDOWN ORDERING CONTRACT (load-bearing — do not reorder these decls): + // `app` is declared AFTER `rt`, so `defer app.deinit()` runs BEFORE + // `defer rt.deinit()` (defers are LIFO). The bridge (owned by `rt`) + // therefore outlives the App's teardown. This matters because the + // release hook below points into the bridge: if the bridge were freed + // first, any later hook invocation would be a use-after-free. + // It is safe today because `App.deinit` frees only its own default + // `kind` boxes and NEVER invokes `override_release_fn` — the surviving + // (non-superseded) Lua overrides are freed by `EventBridge.deinit` when + // `rt.deinit()` runs. The hook fires only during live mid-stream swaps, + // while both App and bridge are alive. If you ever make `App.deinit` + // call the release hook, or move `rt` to outlive `app`, revisit this. + app.setOverrideRelease( + @ptrCast(rt.eventBridge()), + lua_event_bridge.EventBridge.releaseOverrideThunk, + ); + const Flusher = struct { fn flush(ctx: *anyopaque) void { const fw: *std.Io.File.Writer = @ptrCast(@alignCast(ctx)); -- cgit v1.3