Pause status timer while modals are open (#3131)

Summary:
- pause the status timer while waiting on approval modals
- expose deterministic pause/resume helpers to avoid sleep-based tests
- simplify bottom pane timer handling now that the widget owns the clock
This commit is contained in:
Jeremy Rose
2025-09-04 12:37:43 -07:00
committed by GitHub
parent 2073fa7139
commit be23fe1353
2 changed files with 88 additions and 3 deletions

View File

@@ -162,6 +162,8 @@ impl BottomPane {
view.handle_key_event(self, key_event);
if !view.is_complete() {
self.active_view = Some(view);
} else {
self.on_active_view_complete();
}
self.request_redraw();
InputResult::None
@@ -201,6 +203,8 @@ impl BottomPane {
CancellationEvent::Handled => {
if !view.is_complete() {
self.active_view = Some(view);
} else {
self.on_active_view_complete();
}
self.show_ctrl_c_quit_hint();
}
@@ -381,10 +385,27 @@ impl BottomPane {
// Otherwise create a new approval modal overlay.
let modal = ApprovalModalView::new(request, self.app_event_tx.clone());
self.pause_status_timer_for_modal();
self.active_view = Some(Box::new(modal));
self.request_redraw()
}
fn on_active_view_complete(&mut self) {
self.resume_status_timer_after_modal();
}
fn pause_status_timer_for_modal(&mut self) {
if let Some(status) = self.status.as_mut() {
status.pause_timer();
}
}
fn resume_status_timer_after_modal(&mut self) {
if let Some(status) = self.status.as_mut() {
status.resume_timer();
}
}
/// Height (terminal rows) required by the current bottom pane.
pub(crate) fn request_redraw(&self) {
self.frame_requester.schedule_frame();

View File

@@ -26,7 +26,9 @@ pub(crate) struct StatusIndicatorWidget {
/// Queued user messages to display under the status line.
queued_messages: Vec<String>,
start_time: Instant,
elapsed_running: Duration,
last_resume_at: Instant,
is_paused: bool,
app_event_tx: AppEventSender,
frame_requester: FrameRequester,
}
@@ -36,7 +38,9 @@ impl StatusIndicatorWidget {
Self {
header: String::from("Working"),
queued_messages: Vec::new(),
start_time: Instant::now(),
elapsed_running: Duration::ZERO,
last_resume_at: Instant::now(),
is_paused: false,
app_event_tx,
frame_requester,
@@ -88,6 +92,43 @@ impl StatusIndicatorWidget {
// Ensure a redraw so changes are visible.
self.frame_requester.schedule_frame();
}
pub(crate) fn pause_timer(&mut self) {
self.pause_timer_at(Instant::now());
}
pub(crate) fn resume_timer(&mut self) {
self.resume_timer_at(Instant::now());
}
pub(crate) fn pause_timer_at(&mut self, now: Instant) {
if self.is_paused {
return;
}
self.elapsed_running += now.saturating_duration_since(self.last_resume_at);
self.is_paused = true;
}
pub(crate) fn resume_timer_at(&mut self, now: Instant) {
if !self.is_paused {
return;
}
self.last_resume_at = now;
self.is_paused = false;
self.frame_requester.schedule_frame();
}
fn elapsed_seconds_at(&self, now: Instant) -> u64 {
let mut elapsed = self.elapsed_running;
if !self.is_paused {
elapsed += now.saturating_duration_since(self.last_resume_at);
}
elapsed.as_secs()
}
fn elapsed_seconds(&self) -> u64 {
self.elapsed_seconds_at(Instant::now())
}
}
impl WidgetRef for StatusIndicatorWidget {
@@ -99,7 +140,7 @@ impl WidgetRef for StatusIndicatorWidget {
// Schedule next animation frame.
self.frame_requester
.schedule_frame_in(Duration::from_millis(32));
let elapsed = self.start_time.elapsed().as_secs();
let elapsed = self.elapsed_seconds();
// Plain rendering: no borders or padding so the live cell is visually indistinguishable from terminal scrollback.
let mut spans = vec![" ".into()];
@@ -147,6 +188,8 @@ mod tests {
use crate::app_event_sender::AppEventSender;
use ratatui::Terminal;
use ratatui::backend::TestBackend;
use std::time::Duration;
use std::time::Instant;
use tokio::sync::mpsc::unbounded_channel;
#[test]
@@ -191,4 +234,25 @@ mod tests {
.expect("draw");
insta::assert_snapshot!(terminal.backend());
}
#[test]
fn timer_pauses_when_requested() {
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
let tx = AppEventSender::new(tx_raw);
let mut widget = StatusIndicatorWidget::new(tx, crate::tui::FrameRequester::test_dummy());
let baseline = Instant::now();
widget.last_resume_at = baseline;
let before_pause = widget.elapsed_seconds_at(baseline + Duration::from_secs(5));
assert_eq!(before_pause, 5);
widget.pause_timer_at(baseline + Duration::from_secs(5));
let paused_elapsed = widget.elapsed_seconds_at(baseline + Duration::from_secs(10));
assert_eq!(paused_elapsed, before_pause);
widget.resume_timer_at(baseline + Duration::from_secs(10));
let after_resume = widget.elapsed_seconds_at(baseline + Duration::from_secs(13));
assert_eq!(after_resume, before_pause + 3);
}
}