feat: UI animation (#3590)

Add NUX animation

---------

Co-authored-by: Thibault Sottiaux <tibo@openai.com>
This commit is contained in:
jimmyfraiture2
2025-09-15 01:42:17 +01:00
committed by GitHub
parent 2aa84b8891
commit 76c37c5493
291 changed files with 7949 additions and 10 deletions

View File

@@ -5,6 +5,8 @@ use codex_core::config::SWIFTFOX_MODEL_DISPLAY_NAME;
use color_eyre::eyre::Result;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyModifiers;
use rand::Rng as _;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::prelude::Widget;
@@ -14,8 +16,72 @@ use ratatui::widgets::Clear;
use ratatui::widgets::Paragraph;
use ratatui::widgets::WidgetRef;
use ratatui::widgets::Wrap;
use std::time::Duration;
use tokio_stream::StreamExt;
// Embed animation frames for each variant at compile time.
macro_rules! frames_for {
($dir:literal) => {
[
include_str!(concat!("../frames/", $dir, "/frame_000.txt")),
include_str!(concat!("../frames/", $dir, "/frame_004.txt")),
include_str!(concat!("../frames/", $dir, "/frame_008.txt")),
include_str!(concat!("../frames/", $dir, "/frame_012.txt")),
include_str!(concat!("../frames/", $dir, "/frame_016.txt")),
include_str!(concat!("../frames/", $dir, "/frame_020.txt")),
include_str!(concat!("../frames/", $dir, "/frame_024.txt")),
include_str!(concat!("../frames/", $dir, "/frame_028.txt")),
include_str!(concat!("../frames/", $dir, "/frame_032.txt")),
include_str!(concat!("../frames/", $dir, "/frame_036.txt")),
include_str!(concat!("../frames/", $dir, "/frame_040.txt")),
include_str!(concat!("../frames/", $dir, "/frame_044.txt")),
include_str!(concat!("../frames/", $dir, "/frame_048.txt")),
include_str!(concat!("../frames/", $dir, "/frame_052.txt")),
include_str!(concat!("../frames/", $dir, "/frame_056.txt")),
include_str!(concat!("../frames/", $dir, "/frame_060.txt")),
include_str!(concat!("../frames/", $dir, "/frame_064.txt")),
include_str!(concat!("../frames/", $dir, "/frame_068.txt")),
include_str!(concat!("../frames/", $dir, "/frame_072.txt")),
include_str!(concat!("../frames/", $dir, "/frame_076.txt")),
include_str!(concat!("../frames/", $dir, "/frame_080.txt")),
include_str!(concat!("../frames/", $dir, "/frame_084.txt")),
include_str!(concat!("../frames/", $dir, "/frame_088.txt")),
include_str!(concat!("../frames/", $dir, "/frame_092.txt")),
include_str!(concat!("../frames/", $dir, "/frame_096.txt")),
include_str!(concat!("../frames/", $dir, "/frame_100.txt")),
include_str!(concat!("../frames/", $dir, "/frame_104.txt")),
include_str!(concat!("../frames/", $dir, "/frame_108.txt")),
include_str!(concat!("../frames/", $dir, "/frame_112.txt")),
]
};
}
const FRAMES_DEFAULT: [&str; 29] = frames_for!("default");
const FRAMES_CODEX: [&str; 29] = frames_for!("codex");
const FRAMES_OPENAI: [&str; 29] = frames_for!("openai");
const FRAMES_BLOCKS: [&str; 29] = frames_for!("blocks");
const FRAMES_DOTS: [&str; 29] = frames_for!("dots");
const FRAMES_HASH: [&str; 29] = frames_for!("hash");
const FRAMES_HBARS: [&str; 29] = frames_for!("hbars");
const FRAMES_VBARS: [&str; 29] = frames_for!("vbars");
const FRAMES_SHAPES: [&str; 29] = frames_for!("shapes");
const FRAMES_SLUG: [&str; 29] = frames_for!("slug");
const VARIANTS: &[&[&str]] = &[
&FRAMES_DEFAULT,
&FRAMES_CODEX,
&FRAMES_OPENAI,
&FRAMES_BLOCKS,
&FRAMES_DOTS,
&FRAMES_HASH,
&FRAMES_HBARS,
&FRAMES_VBARS,
&FRAMES_SHAPES,
&FRAMES_SLUG,
];
const FRAME_TICK: Duration = Duration::from_millis(60);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum ModelUpgradeDecision {
Switch,
@@ -32,6 +98,8 @@ struct ModelUpgradePopup {
highlighted: ModelUpgradeOption,
decision: Option<ModelUpgradeDecision>,
request_frame: FrameRequester,
frame_idx: usize,
variant_idx: usize,
}
impl ModelUpgradePopup {
@@ -40,6 +108,8 @@ impl ModelUpgradePopup {
highlighted: ModelUpgradeOption::TryNewModel,
decision: None,
request_frame,
frame_idx: 0,
variant_idx: 0,
}
}
@@ -51,6 +121,11 @@ impl ModelUpgradePopup {
KeyCode::Char('2') => self.select(ModelUpgradeOption::KeepCurrent),
KeyCode::Enter => self.select(self.highlighted),
KeyCode::Esc => self.select(ModelUpgradeOption::KeepCurrent),
KeyCode::Char('.') => {
if key_event.modifiers.contains(KeyModifiers::CONTROL) {
self.pick_random_variant();
}
}
_ => {}
}
}
@@ -66,6 +141,30 @@ impl ModelUpgradePopup {
self.decision = Some(option.into());
self.request_frame.schedule_frame();
}
fn advance_animation(&mut self) {
let len = self.frames().len();
self.frame_idx = (self.frame_idx + 1) % len;
self.request_frame.schedule_frame_in(FRAME_TICK);
}
fn frames(&self) -> &'static [&'static str] {
VARIANTS[self.variant_idx]
}
fn pick_random_variant(&mut self) {
let total = VARIANTS.len();
if total <= 1 {
return;
}
let mut rng = rand::rng();
let mut next = self.variant_idx;
while next == self.variant_idx {
next = rng.random_range(0..total);
}
self.variant_idx = next;
self.request_frame.schedule_frame();
}
}
impl From<ModelUpgradeOption> for ModelUpgradeDecision {
@@ -81,20 +180,27 @@ impl WidgetRef for &ModelUpgradePopup {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
Clear.render(area, buf);
let mut lines: Vec<Line> = vec![
String::new().into(),
let mut lines: Vec<Line> = self.frames()[self.frame_idx]
.lines()
.map(|l| l.to_string().into())
.collect();
// Spacer between animation and text content.
lines.push("".into());
lines.push(
format!(
" Codex is now powered by {SWIFTFOX_MODEL_DISPLAY_NAME}, a new model that is"
)
.into(),
Line::from(vec![
" ".into(),
"faster, a better collaborator, ".bold(),
"and ".into(),
"more steerable.".bold(),
]),
"".into(),
];
);
lines.push(Line::from(vec![
" ".into(),
"faster, a better collaborator, ".bold(),
"and ".into(),
"more steerable.".bold(),
]));
lines.push("".into());
let create_option =
|index: usize, option: ModelUpgradeOption, text: &str| -> Line<'static> {
@@ -138,6 +244,8 @@ pub(crate) async fn run_model_upgrade_popup(tui: &mut Tui) -> Result<ModelUpgrad
frame.render_widget_ref(&popup, frame.area());
})?;
popup.advance_animation();
let events = tui.event_stream();
tokio::pin!(events);
while popup.decision.is_none() {
@@ -145,6 +253,7 @@ pub(crate) async fn run_model_upgrade_popup(tui: &mut Tui) -> Result<ModelUpgrad
match event {
TuiEvent::Key(key_event) => popup.handle_key_event(key_event),
TuiEvent::Draw => {
popup.advance_animation();
let _ = tui.draw(u16::MAX, |frame| {
frame.render_widget_ref(&popup, frame.area());
});