Files
codex/codex-rs/tui/src/summary_shimmer.rs
T
Felipe Coury 71b38795d8 Match TUI status shimmer timing to desktop headers (#48757)
## What changed

Replace the continuous two-second shimmer with a 600 ms initial delay and a one-second sweep every four seconds, matching the desktop app's thinking and reasoning header timings.

## Testing

Update snapshots for short, long, and Unicode labels in both themes to cover the initial delay, sweep, gap, and next sweep. Sample smoothness across two sweeps at 32 ms intervals and adjust the brightness-change bound for the faster sweep.

GitOrigin-RevId: d7a6a81db99bf0e8a73eee0c67eb7e44d4d76f42
2026-09-27 17:59:57 +00:00

65 lines
2.5 KiB
Rust

//! Smooth, whole-grapheme status shimmer with the desktop's cadenced sweep.
//!
//! Only brightness changes: the moving band uses the terminal foreground, while
//! the remaining text blends halfway into the background. The wave spans at
//! least six terminal columns so short labels do not flash one letter at a time.
//! New headers wait 600 ms, then sweep for one second every four seconds.
//! Unknown palettes use static dim text instead of a stepped animation.
use std::time::Duration;
use ratatui::style::Style;
use ratatui::text::Span;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use crate::color::blend;
use crate::motion::MotionMode;
use crate::terminal_palette::StdoutColorLevel;
use crate::terminal_palette::default_bg;
use crate::terminal_palette::default_fg;
use crate::terminal_palette::effective_stdout_color_level;
use crate::terminal_palette::rgb_color;
// Match the desktop app's thinking and reasoning header timings.
const START_DELAY: Duration = Duration::from_millis(/*millis*/ 600);
const SWEEP_SECONDS: f64 = 1.0;
const INTERVAL_SECONDS: f64 = 4.0;
pub(crate) fn summary_shimmer(
text: &str,
elapsed: Duration,
motion: MotionMode,
) -> Vec<Span<'static>> {
if motion == MotionMode::Reduced {
return vec![text.to_owned().into()];
}
let (StdoutColorLevel::TrueColor, Some(fg), Some(bg)) =
(effective_stdout_color_level(), default_fg(), default_bg())
else {
return vec![Span::styled(text.to_owned(), Style::default().dim())];
};
let width = text.width() as f64;
let half_width = (width * 0.1).max(/*other*/ 3.0);
let sweep =
(elapsed.saturating_sub(START_DELAY).as_secs_f64() % INTERVAL_SECONDS).min(SWEEP_SECONDS);
let position = sweep / SWEEP_SECONDS * (width + 2.0 * half_width) - half_width;
let mut column = 0.0;
text.graphemes(/*is_extended*/ true)
.map(|grapheme| {
let glyph_width = grapheme.width() as f64;
let center = column + glyph_width / 2.0;
column += glyph_width;
let distance = ((center - position).abs() / half_width).min(/*other*/ 1.0);
let intensity = 0.5 * (1.0 + (std::f64::consts::PI * distance).cos());
let alpha = (0.5 + 0.5 * intensity) as f32;
let style = Style::default().fg(rgb_color(blend(fg, bg, alpha)));
Span::styled(grapheme.to_owned(), style)
})
.collect()
}
#[cfg(test)]
#[path = "summary_shimmer_tests.rs"]
mod tests;