add pulsing dot loading state (#4736)

## Description 
Changes default CLI spinner to pulsing dot


https://github.com/user-attachments/assets/b81225d6-6655-4ead-8cb1-d6568a603d5b

## Tests
Passes CI

---------

Co-authored-by: Fouad Matin <fouad@openai.com>
This commit is contained in:
Ed Bayes
2025-10-06 09:56:27 +05:30
committed by GitHub
parent c264ae6021
commit d3e1beb26c
10 changed files with 84 additions and 70 deletions

View File

@@ -2,5 +2,5 @@
source: tui/src/chatwidget/tests.rs
expression: blob1
---
Exploring
Exploring
└ List ls -la

View File

@@ -2,6 +2,6 @@
source: tui/src/chatwidget/tests.rs
expression: blob3
---
Exploring
Exploring
└ List ls -la
Read foo.txt

View File

@@ -116,12 +116,10 @@ pub(crate) fn output_lines(
}
pub(crate) fn spinner(start_time: Option<Instant>) -> Span<'static> {
const FRAMES: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
let idx = start_time
.map(|st| ((st.elapsed().as_millis() / 100) as usize) % FRAMES.len())
.unwrap_or(0);
let ch = FRAMES[idx];
ch.to_string().into()
let blink_on = start_time
.map(|st| ((st.elapsed().as_millis() / 600) % 2) == 0)
.unwrap_or(false);
if blink_on { "".into() } else { "".dim() }
}
impl HistoryCell for ExecCell {

View File

@@ -3,4 +3,4 @@ source: tui/src/history_cell.rs
assertion_line: 1740
expression: rendered
---
Calling search.find_docs({"query":"ratatui styling","limit":3})
Calling search.find_docs({"query":"ratatui styling","limit":3})

View File

@@ -134,12 +134,16 @@ impl StatusIndicatorWidget {
self.frame_requester.schedule_frame();
}
fn elapsed_seconds_at(&self, now: Instant) -> u64 {
fn elapsed_duration_at(&self, now: Instant) -> Duration {
let mut elapsed = self.elapsed_running;
if !self.is_paused {
elapsed += now.saturating_duration_since(self.last_resume_at);
}
elapsed.as_secs()
elapsed
}
fn elapsed_seconds_at(&self, now: Instant) -> u64 {
self.elapsed_duration_at(now).as_secs()
}
pub fn elapsed_seconds(&self) -> u64 {
@@ -156,11 +160,18 @@ impl WidgetRef for StatusIndicatorWidget {
// Schedule next animation frame.
self.frame_requester
.schedule_frame_in(Duration::from_millis(32));
let elapsed = self.elapsed_seconds();
let pretty_elapsed = fmt_elapsed_compact(elapsed);
let now = Instant::now();
let elapsed_duration = self.elapsed_duration_at(now);
let pretty_elapsed = fmt_elapsed_compact(elapsed_duration.as_secs());
let blink_on = (elapsed_duration.as_millis() / 600).is_multiple_of(2);
// Plain rendering: no borders or padding so the live cell is visually indistinguishable from terminal scrollback.
let mut spans = vec!["".dim()];
let mut spans = Vec::with_capacity(5);
if blink_on {
spans.push("".into());
} else {
spans.push("".dim());
}
spans.extend(shimmer_spans(&self.header));
spans.extend(vec![
" ".into(),