From ded36c1007f4f68fae878af60df0eaec639f30f5 Mon Sep 17 00:00:00 2001 From: TimBot Date: Sun, 14 Jun 2026 07:45:34 -0500 Subject: [PATCH] vxfw: unregister resize handler when app exits App.run installs a SIGWINCH resize callback when the terminal does not support in-band resize. The callback points at the run-local event loop, but it was never removed before that loop went out of scope. Repeated App init/deinit cycles could therefore leave stale callbacks registered until the fixed handler array filled and notifyWinsize returned OutOfMemory. Track whether a loop installed the resize handler, unregister it on exit, and reset the process SIGWINCH handler once the last registered callback is removed. --- src/Loop.zig | 29 +++++++++++++++++++++++------ src/tty.zig | 46 ++++++++++++++++++++++++++++++++++++++++++++++ src/vxfw/App.zig | 14 +++++++------- 3 files changed, 76 insertions(+), 13 deletions(-) diff --git a/src/Loop.zig b/src/Loop.zig index f2c21d8a..7906358a 100644 --- a/src/Loop.zig +++ b/src/Loop.zig @@ -23,6 +23,7 @@ pub fn Loop(comptime T: type) type { queue: Queue(T, 512), thread: ?std.Io.Future(void) = null, should_quit: bool = false, + resize_handler_installed: bool = false, /// Initialize the event loop. This is an intrusive init so that we have /// a stable pointer to register signal callbacks with posix TTYs @@ -39,17 +40,33 @@ pub fn Loop(comptime T: type) type { switch (builtin.os.tag) { .windows => {}, else => { - if (!builtin.is_test) { - const handler: Tty.SignalHandler = .{ - .context = self, - .callback = Self.winsizeCallback, - }; - try Tty.notifyWinsize(handler); + if (!builtin.is_test and !self.resize_handler_installed) { + try Tty.notifyWinsize(self.resizeHandler()); + self.resize_handler_installed = true; } }, } } + pub fn uninstallResizeHandler(self: *Self) void { + switch (builtin.os.tag) { + .windows => {}, + else => { + if (!builtin.is_test and self.resize_handler_installed) { + Tty.removeWinsize(self.resizeHandler()); + self.resize_handler_installed = false; + } + }, + } + } + + fn resizeHandler(self: *Self) Tty.SignalHandler { + return .{ + .context = self, + .callback = Self.winsizeCallback, + }; + } + /// spawns the input thread to read input from the tty pub fn start(self: *Self) !void { if (self.thread) |_| return; diff --git a/src/tty.zig b/src/tty.zig index 2e1b17d7..18dd5194 100644 --- a/src/tty.zig +++ b/src/tty.zig @@ -98,6 +98,18 @@ pub const PosixTty = struct { /// Resets the signal handler to it's default pub fn resetSignalHandler() void { + if (!handler_installed) { + handler_idx = 0; + return; + } + + handler_mutex.lock(handler_io) catch @panic("unable to lock SIGWINCH handlers"); + defer handler_mutex.unlock(handler_io); + handler_idx = 0; + resetSignalHandlerLocked(); + } + + fn resetSignalHandlerLocked() void { if (!handler_installed) return; handler_installed = false; var act = posix.Sigaction{ @@ -129,6 +141,25 @@ pub const PosixTty = struct { handler_idx += 1; } + /// Remove a previously installed winsize signal handler + pub fn removeWinsize(handler: SignalHandler) void { + handler_mutex.lock(handler_io) catch @panic("unable to lock SIGWINCH handlers"); + defer handler_mutex.unlock(handler_io); + + var i: usize = 0; + while (i < handler_idx) : (i += 1) { + if (handlers[i].context == handler.context and handlers[i].callback == handler.callback) { + handler_idx -= 1; + if (i < handler_idx) { + std.mem.copyForwards(SignalHandler, handlers[i..handler_idx], handlers[i + 1 .. handler_idx + 1]); + } + handlers[handler_idx] = undefined; + if (handler_idx == 0) resetSignalHandlerLocked(); + return; + } + } + } + fn handleWinch(_: std.posix.SIG) callconv(.c) void { handler_mutex.lock(handler_io) catch @panic("unable to lock SIGWINCH handlers"); defer handler_mutex.unlock(handler_io); @@ -211,6 +242,11 @@ pub const WindowsTty = struct { const utf8_codepage: c_uint = 65001; + pub const SignalHandler = struct { + context: *anyopaque, + callback: *const fn (context: *anyopaque) void, + }; + /// The input mode set by init pub const input_raw_mode: CONSOLE_MODE_INPUT = .{ .WINDOW_INPUT = 1, // resize events @@ -757,6 +793,11 @@ pub const TestTty = switch (builtin.os.tag) { pipe_write: posix.fd_t, tty_writer: *std.Io.Writer.Allocating, + pub const SignalHandler = struct { + context: *anyopaque, + callback: *const fn (context: *anyopaque) void, + }; + /// Initializes a TestTty. pub fn init(_: std.Io, buffer: []u8) !@This() { _ = buffer; @@ -814,6 +855,11 @@ pub const TestTty = switch (builtin.os.tag) { else => struct { d: std.Io.Writer.Discarding, + pub const SignalHandler = struct { + context: *anyopaque, + callback: *const fn (context: *anyopaque) void, + }; + pub fn init(_: std.Io, buf: []u8) !@This() { return .{ .d = .init(buf), diff --git a/src/vxfw/App.zig b/src/vxfw/App.zig index 1d0ae947..03c5eb3b 100644 --- a/src/vxfw/App.zig +++ b/src/vxfw/App.zig @@ -67,13 +67,13 @@ pub fn run(self: *App, widget: vxfw.Widget, opts: Options) anyerror!void { try vx.setBracketedPaste(tty.writer(), true); try vx.subscribeToColorSchemeUpdates(tty.writer()); - { - // This part deserves a comment. loop.installResizeHandler installs - // a signal handler for the tty. We wait to installResizeHandler the - // loop until we know if we need this handler. We don't need it if the - // terminal supports in-band-resize. - if (!vx.state.in_band_resize) try loop.installResizeHandler(); - } + // This part deserves a comment. loop.installResizeHandler installs + // a signal handler for the tty. We wait to installResizeHandler the + // loop until we know if we need this handler. We don't need it if the + // terminal supports in-band-resize. + const use_signal_resize = !vx.state.in_band_resize; + if (use_signal_resize) try loop.installResizeHandler(); + defer if (use_signal_resize) loop.uninstallResizeHandler(); // NOTE: We don't use pixel mouse anywhere vx.caps.sgr_pixels = false;