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:
@@ -162,6 +162,8 @@ impl BottomPane {
|
|||||||
view.handle_key_event(self, key_event);
|
view.handle_key_event(self, key_event);
|
||||||
if !view.is_complete() {
|
if !view.is_complete() {
|
||||||
self.active_view = Some(view);
|
self.active_view = Some(view);
|
||||||
|
} else {
|
||||||
|
self.on_active_view_complete();
|
||||||
}
|
}
|
||||||
self.request_redraw();
|
self.request_redraw();
|
||||||
InputResult::None
|
InputResult::None
|
||||||
@@ -201,6 +203,8 @@ impl BottomPane {
|
|||||||
CancellationEvent::Handled => {
|
CancellationEvent::Handled => {
|
||||||
if !view.is_complete() {
|
if !view.is_complete() {
|
||||||
self.active_view = Some(view);
|
self.active_view = Some(view);
|
||||||
|
} else {
|
||||||
|
self.on_active_view_complete();
|
||||||
}
|
}
|
||||||
self.show_ctrl_c_quit_hint();
|
self.show_ctrl_c_quit_hint();
|
||||||
}
|
}
|
||||||
@@ -381,10 +385,27 @@ impl BottomPane {
|
|||||||
|
|
||||||
// Otherwise create a new approval modal overlay.
|
// Otherwise create a new approval modal overlay.
|
||||||
let modal = ApprovalModalView::new(request, self.app_event_tx.clone());
|
let modal = ApprovalModalView::new(request, self.app_event_tx.clone());
|
||||||
|
self.pause_status_timer_for_modal();
|
||||||
self.active_view = Some(Box::new(modal));
|
self.active_view = Some(Box::new(modal));
|
||||||
self.request_redraw()
|
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.
|
/// Height (terminal rows) required by the current bottom pane.
|
||||||
pub(crate) fn request_redraw(&self) {
|
pub(crate) fn request_redraw(&self) {
|
||||||
self.frame_requester.schedule_frame();
|
self.frame_requester.schedule_frame();
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ pub(crate) struct StatusIndicatorWidget {
|
|||||||
/// Queued user messages to display under the status line.
|
/// Queued user messages to display under the status line.
|
||||||
queued_messages: Vec<String>,
|
queued_messages: Vec<String>,
|
||||||
|
|
||||||
start_time: Instant,
|
elapsed_running: Duration,
|
||||||
|
last_resume_at: Instant,
|
||||||
|
is_paused: bool,
|
||||||
app_event_tx: AppEventSender,
|
app_event_tx: AppEventSender,
|
||||||
frame_requester: FrameRequester,
|
frame_requester: FrameRequester,
|
||||||
}
|
}
|
||||||
@@ -36,7 +38,9 @@ impl StatusIndicatorWidget {
|
|||||||
Self {
|
Self {
|
||||||
header: String::from("Working"),
|
header: String::from("Working"),
|
||||||
queued_messages: Vec::new(),
|
queued_messages: Vec::new(),
|
||||||
start_time: Instant::now(),
|
elapsed_running: Duration::ZERO,
|
||||||
|
last_resume_at: Instant::now(),
|
||||||
|
is_paused: false,
|
||||||
|
|
||||||
app_event_tx,
|
app_event_tx,
|
||||||
frame_requester,
|
frame_requester,
|
||||||
@@ -88,6 +92,43 @@ impl StatusIndicatorWidget {
|
|||||||
// Ensure a redraw so changes are visible.
|
// Ensure a redraw so changes are visible.
|
||||||
self.frame_requester.schedule_frame();
|
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 {
|
impl WidgetRef for StatusIndicatorWidget {
|
||||||
@@ -99,7 +140,7 @@ impl WidgetRef for StatusIndicatorWidget {
|
|||||||
// Schedule next animation frame.
|
// Schedule next animation frame.
|
||||||
self.frame_requester
|
self.frame_requester
|
||||||
.schedule_frame_in(Duration::from_millis(32));
|
.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.
|
// Plain rendering: no borders or padding so the live cell is visually indistinguishable from terminal scrollback.
|
||||||
let mut spans = vec![" ".into()];
|
let mut spans = vec![" ".into()];
|
||||||
@@ -147,6 +188,8 @@ mod tests {
|
|||||||
use crate::app_event_sender::AppEventSender;
|
use crate::app_event_sender::AppEventSender;
|
||||||
use ratatui::Terminal;
|
use ratatui::Terminal;
|
||||||
use ratatui::backend::TestBackend;
|
use ratatui::backend::TestBackend;
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::time::Instant;
|
||||||
use tokio::sync::mpsc::unbounded_channel;
|
use tokio::sync::mpsc::unbounded_channel;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -191,4 +234,25 @@ mod tests {
|
|||||||
.expect("draw");
|
.expect("draw");
|
||||||
insta::assert_snapshot!(terminal.backend());
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user