git.lucas.co / cce-terminal
terminal emulator
git clone https://git.lucas.co/cce-terminal.git

commit2e92f2491504dde5ececfd6867c790c8e5fd4fde
parent3c3c0361e2
authorLucas Galante <[email protected]>
date2026-09-11 13:50
Ask for frames while the bell fades and autoscroll runs

Two decays in `tick` age in real time but ask the runner for nothing:
a background tab's bell flash (only the active tab's decay requests a
frame) and selection autoscroll with the pointer held still in the edge
padding (motion events stop at the edge, which is the whole reason that
scroll is time-driven). With no frame pending the runner parks on its
1 s idle sleep and clamps `dt` to one frame per wake, so autoscroll
crawled between the 200 ms warm windows and a background bell had not
faded by the time its tab was shown.

Override `idle_poll_interval` to frame cadence while either is live and
nothing otherwise, and lift the overshoot test into
`autoscroll_overshoot` so `tick` and the override cannot drift apart.

Co-Authored-By: Claude Opus 5 <[email protected]>

 src/main.rs | 42 +++++++++++++++++++++++++++++++++---------
 1 file changed, 33 insertions(+), 9 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index b8c732a..6649b80 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -543,6 +543,24 @@ impl TerminalApp {
         }
     }
 
+    /// How far a selection drag is holding the pointer past the top or
+    /// bottom edge padding: 0 when it is inside, negative above, positive
+    /// below. Drives the autoscroll rate in `tick`, and the frame cadence
+    /// `idle_poll_interval` has to ask for while it is non-zero.
+    fn autoscroll_overshoot(&self) -> f32 {
+        if !self.selecting {
+            return 0.0;
+        }
+        let y = self.last_pointer.y as f32;
+        if y < self.pad {
+            self.pad - y
+        } else if y > self.win_h - self.pad {
+            (self.win_h - self.pad) - y
+        } else {
+            0.0
+        }
+    }
+
     /// The scrollback offset's range: 0 (live bottom) ..= history length.
     fn scrollback_bounds(&self) -> Bounds {
         Bounds::max(self.tab().term.grid().history_size() as f32)
@@ -910,6 +928,19 @@ impl Application for TerminalApp {
         }
     }
 
+    /// Two things age in `tick` without redrawing anything the runner can
+    /// see: a background tab's bell flash (only the active tab's decay asks
+    /// for a frame) and selection autoscroll with the pointer held still in
+    /// the edge padding (motion events stop at the edge). Both are real-time
+    /// decays, and the runner's idle sleep clamps `dt` to one frame per
+    /// wake — so ask for frame cadence while either is live, and nothing
+    /// otherwise.
+    fn idle_poll_interval(&self) -> Option<std::time::Duration> {
+        let bell = self.tabs.iter().any(|t| t.bell > 0.0);
+        (bell || self.autoscroll_overshoot() != 0.0)
+            .then(|| std::time::Duration::from_millis(16))
+    }
+
     fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
         // Bell flash decay — every tab's, so a background tab's flash has
         // faded by the time it is shown rather than greeting the switch.
@@ -937,15 +968,8 @@ impl Application for TerminalApp {
         // Selection autoscroll: while a drag holds the pointer in the top or
         // bottom edge padding, scroll at a rate scaling with the overshoot
         // (motion events stop at the edge, so this is time-driven).
-        if self.selecting {
-            let y = self.last_pointer.y as f32;
-            let overshoot = if y < self.pad {
-                self.pad - y
-            } else if y > self.win_h - self.pad {
-                (self.win_h - self.pad) - y
-            } else {
-                0.0
-            };
+        {
+            let overshoot = self.autoscroll_overshoot();
             if overshoot != 0.0 {
                 let rate = 4.0 + overshoot.abs().min(24.0) * 2.0; // lines/sec
                 self.autoscroll_accum += dt * rate * overshoot.signum();