Files
llmx/codex-rs/tui/src/shimmer.rs
ae 5bce369c4d fix: clean up styles & colors and define in styles.md (#2401)
New style guide:

  # Headers, primary, and secondary text
  
- **Headers:** Use `bold`. For markdown with various header levels,
leave in the `#` signs.
  - **Primary text:** Default.
  - **Secondary text:** Use `dim`.
  
  # Foreground colors
  
- **Default:** Most of the time, just use the default foreground color.
`reset` can help get it back.
- **Selection:** Use ANSI `blue`. (Ed & AE want to make this cyan too,
but we'll do that in a followup since it's riskier in different themes.)
  - **User input tips and status indicators:** Use ANSI `cyan`.
  - **Success and additions:** Use ANSI `green`.
  - **Errors, failures and deletions:** Use ANSI `red`.
  - **Codex:** Use ANSI `magenta`.
  
  # Avoid
  
- Avoid custom colors because there's no guarantee that they'll contrast
well or look good on various terminal color themes.
- Avoid ANSI `black`, `white`, `yellow` as foreground colors because the
terminal theme will do a better job. (Use `reset` if you need to in
order to get those.) The exception is if you need contrast rendering
over a manually colored background.
  
  (There are some rules to try to catch this in `clippy.toml`.)

# Testing

Tested in a variety of light and dark color themes in Terminal, iTerm2, and Ghostty.
2025-08-18 08:26:29 -07:00

74 lines
2.3 KiB
Rust

use std::sync::OnceLock;
use std::time::Duration;
use std::time::Instant;
use ratatui::style::Color;
use ratatui::style::Modifier;
use ratatui::style::Style;
use ratatui::text::Span;
static PROCESS_START: OnceLock<Instant> = OnceLock::new();
fn elapsed_since_start() -> Duration {
let start = PROCESS_START.get_or_init(Instant::now);
start.elapsed()
}
pub(crate) fn shimmer_spans(text: &str) -> Vec<Span<'static>> {
let chars: Vec<char> = text.chars().collect();
if chars.is_empty() {
return Vec::new();
}
// Use time-based sweep synchronized to process start.
let padding = 10usize;
let period = chars.len() + padding * 2;
let sweep_seconds = 2.5f32;
let pos_f =
(elapsed_since_start().as_secs_f32() % sweep_seconds) / sweep_seconds * (period as f32);
let pos = pos_f as usize;
let has_true_color = supports_color::on_cached(supports_color::Stream::Stdout)
.map(|level| level.has_16m)
.unwrap_or(false);
let band_half_width = 3.0;
let mut spans: Vec<Span<'static>> = Vec::with_capacity(chars.len());
for (i, ch) in chars.iter().enumerate() {
let i_pos = i as isize + padding as isize;
let pos = pos as isize;
let dist = (i_pos - pos).abs() as f32;
let t = if dist <= band_half_width {
let x = std::f32::consts::PI * (dist / band_half_width);
0.5 * (1.0 + x.cos())
} else {
0.0
};
let brightness = 0.4 + 0.6 * t;
let level = (brightness * 255.0).clamp(0.0, 255.0) as u8;
let style = if has_true_color {
// Allow custom RGB colors, as the implementation is thoughtfully
// adjusting the level of the default foreground color.
#[allow(clippy::disallowed_methods)]
{
Style::default()
.fg(Color::Rgb(level, level, level))
.add_modifier(Modifier::BOLD)
}
} else {
color_for_level(level)
};
spans.push(Span::styled(ch.to_string(), style));
}
spans
}
fn color_for_level(level: u8) -> Style {
if level < 144 {
Style::default().add_modifier(Modifier::DIM)
} else if level < 208 {
Style::default()
} else {
Style::default().add_modifier(Modifier::BOLD)
}
}