From 2ebbe39b9ee818b3cc908d966256af4df9a38caa Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 13 Oct 2025 20:44:32 -0700 Subject: [PATCH] termio: don't start scroll timer if its already active (#9195) This might fix #9191, but since I don't have a reproduction I can't be sure. In any case, this is a bad bug that should be fixed. The issue is that we weren't checking our scroll timer completion state. This meant that if `start_scroll_timer` was called multiple times within a single loop tick, we'd enqueue our completion multiple times, leading to various undefined behaviors. If we don't enqueue anything else in the interim, this is safe by chance. But if we enqueue something else, then we'd hit a queue assertion failure and honestly I'm not totally sure what would happen. I wasn't able to trigger the "bad" case, but I was able to trigger the benign case very easily. Our other timers such as the renderer cursor timer already have this protection. Let's fix this and continue looking... --- src/termio/Thread.zig | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/termio/Thread.zig b/src/termio/Thread.zig index edf966df7..bb616e623 100644 --- a/src/termio/Thread.zig +++ b/src/termio/Thread.zig @@ -471,15 +471,23 @@ fn stopCallback( fn startScrollTimer(self: *Thread, cb: *CallbackData) void { self.scroll_active = true; - // Start the timer which loops - self.scroll.run( - &self.loop, - &self.scroll_c, - selection_scroll_ms, - CallbackData, - cb, - selectionScrollCallback, - ); + switch (self.scroll_c.state()) { + // If it is already active, e.g. startScrollTimer is called multiple + // times, then we just return. We can't simply check `scroll_active` + // because its possible that `stopScrollTimer` was called but there + // was no loop tick between then and now to halt out completion. + .active => return, + + // If the completion is not active then we need to start it. + .dead => self.scroll.run( + &self.loop, + &self.scroll_c, + selection_scroll_ms, + CallbackData, + cb, + selectionScrollCallback, + ), + } } fn stopScrollTimer(self: *Thread) void {