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
This commit is contained in:
Felipe Coury
2026-09-27 17:59:57 +00:00
committed by copyberry
parent 819cdb726d
commit 71b38795d8
3 changed files with 48 additions and 31 deletions
@@ -1,35 +1,40 @@
---
source: tui/src/summary_shimmer_tests.rs
assertion_line: 27
expression: "frames.join(\"\\n\")"
---
dark Working 0ms: [128, 128, 128, 128, 128, 128, 128]
dark Working 500ms: [238, 198, 144, 128, 128, 128, 128]
dark Working 1000ms: [128, 156, 212, 240, 212, 156, 128]
dark Working 1500ms: [128, 128, 128, 128, 144, 198, 238]
dark Working 2000ms: [128, 128, 128, 128, 128, 128, 128]
dark Working 850ms: [238, 198, 144, 128, 128, 128, 128]
dark Working 1100ms: [128, 156, 212, 240, 212, 156, 128]
dark Working 1350ms: [128, 128, 128, 128, 144, 198, 238]
dark Working 3600ms: [128, 128, 128, 128, 128, 128, 128]
dark Working 5100ms: [128, 156, 212, 240, 212, 156, 128]
dark Preparing bootstrap diagnostics 0ms: [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128]
dark Preparing bootstrap diagnostics 500ms: [128, 128, 128, 132, 175, 226, 237, 198, 145, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128]
dark Preparing bootstrap diagnostics 1000ms: [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 159, 213, 240, 213, 159, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128]
dark Preparing bootstrap diagnostics 1500ms: [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 145, 198, 237, 226, 175, 132, 128, 128, 128]
dark Preparing bootstrap diagnostics 2000ms: [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128]
dark Preparing bootstrap diagnostics 850ms: [128, 128, 128, 132, 175, 226, 237, 198, 145, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128]
dark Preparing bootstrap diagnostics 1100ms: [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 159, 213, 240, 213, 159, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128]
dark Preparing bootstrap diagnostics 1350ms: [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 145, 198, 237, 226, 175, 132, 128, 128, 128]
dark Preparing bootstrap diagnostics 3600ms: [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128]
dark Preparing bootstrap diagnostics 5100ms: [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 159, 213, 240, 213, 159, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128]
dark 界é界 0ms: [128, 128, 128]
dark 界é界 500ms: [198, 129, 128]
dark 界é界 1000ms: [184, 240, 184]
dark 界é界 1500ms: [128, 129, 198]
dark 界é界 2000ms: [128, 128, 128]
dark 界é界 850ms: [198, 129, 128]
dark 界é界 1100ms: [184, 240, 184]
dark 界é界 1350ms: [128, 129, 198]
dark 界é界 3600ms: [128, 128, 128]
dark 界é界 5100ms: [184, 240, 184]
light Working 0ms: [128, 128, 128, 128, 128, 128, 128]
light Working 500ms: [17, 57, 111, 128, 128, 128, 128]
light Working 1000ms: [128, 100, 44, 16, 44, 100, 128]
light Working 1500ms: [128, 128, 128, 128, 111, 57, 17]
light Working 2000ms: [128, 128, 128, 128, 128, 128, 128]
light Working 850ms: [17, 57, 111, 128, 128, 128, 128]
light Working 1100ms: [128, 100, 44, 16, 44, 100, 128]
light Working 1350ms: [128, 128, 128, 128, 111, 57, 17]
light Working 3600ms: [128, 128, 128, 128, 128, 128, 128]
light Working 5100ms: [128, 100, 44, 16, 44, 100, 128]
light Preparing bootstrap diagnostics 0ms: [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128]
light Preparing bootstrap diagnostics 500ms: [128, 128, 128, 123, 80, 29, 18, 57, 110, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128]
light Preparing bootstrap diagnostics 1000ms: [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 127, 96, 42, 16, 42, 96, 127, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128]
light Preparing bootstrap diagnostics 1500ms: [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 110, 57, 18, 29, 80, 123, 128, 128, 128]
light Preparing bootstrap diagnostics 2000ms: [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128]
light Preparing bootstrap diagnostics 850ms: [128, 128, 128, 123, 80, 29, 18, 57, 110, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128]
light Preparing bootstrap diagnostics 1100ms: [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 127, 96, 42, 16, 42, 96, 127, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128]
light Preparing bootstrap diagnostics 1350ms: [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 110, 57, 18, 29, 80, 123, 128, 128, 128]
light Preparing bootstrap diagnostics 3600ms: [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128]
light Preparing bootstrap diagnostics 5100ms: [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 127, 96, 42, 16, 42, 96, 127, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128]
light 界é界 0ms: [128, 128, 128]
light 界é界 500ms: [57, 126, 128]
light 界é界 1000ms: [72, 16, 72]
light 界é界 1500ms: [128, 126, 57]
light 界é界 2000ms: [128, 128, 128]
light 界é界 850ms: [57, 126, 128]
light 界é界 1100ms: [72, 16, 72]
light 界é界 1350ms: [128, 126, 57]
light 界é界 3600ms: [128, 128, 128]
light 界é界 5100ms: [72, 16, 72]
+10 -2
View File
@@ -1,8 +1,9 @@
//! Smooth, whole-grapheme status shimmer with a two-second sweep.
//! 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;
@@ -20,6 +21,11 @@ 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,
@@ -35,7 +41,9 @@ pub(crate) fn summary_shimmer(
};
let width = text.width() as f64;
let half_width = (width * 0.1).max(/*other*/ 3.0);
let position = (elapsed.as_secs_f64() % 2.0) / 2.0 * (width + 2.0 * half_width) - half_width;
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| {
+8 -4
View File
@@ -25,7 +25,8 @@ fn short_and_long_labels_sweep_smoothly_in_both_themes() {
] {
with_test_default_colors(colors, || {
for text in ["Working", "Preparing bootstrap diagnostics", "界e\u{301}界"] {
for ms in [0, 500, 1000, 1500, 2000] {
// Initial delay, the first sweep, the gap, and the next sweep.
for ms in [0, 850, 1100, 1350, 3600, 5100] {
let spans =
summary_shimmer(text, Duration::from_millis(ms), MotionMode::Animated);
let levels = spans
@@ -52,7 +53,8 @@ fn working_has_overlapping_highlights_without_frame_to_frame_flashes() {
},
|| {
let mut previous = Vec::new();
for ms in (0..=2000).step_by(/*step*/ 16) {
// Sample two sweeps and the intervening gap at the status row's 32 ms cadence.
for ms in (0..=5600).step_by(/*step*/ 32) {
let spans =
summary_shimmer("Working", Duration::from_millis(ms), MotionMode::Animated);
let brightness = spans
@@ -66,13 +68,15 @@ fn working_has_overlapping_highlights_without_frame_to_frame_flashes() {
assert!(brightness.iter().filter(|value| **value > 160).count() >= 2);
}
for (current, previous) in brightness.iter().zip(&previous) {
assert!(u8::abs_diff(*current, *previous) <= 7);
// At this speed the cosine band's maximum change over 32 ms is
// 112 * sin(PI * (13 * 0.032) / 6), rounded up.
assert!(u8::abs_diff(*current, *previous) <= 25);
}
previous = brightness;
}
let midpoint = summary_shimmer(
"Working",
Duration::from_secs(/*secs*/ 1),
Duration::from_millis(/*millis*/ 1100),
MotionMode::Animated,
);
let expected = "Working"