Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add UI animation to NUX
  • Loading branch information
jimmyfraiture2 committed Sep 14, 2025
commit 5139973b07379845d433402769f6c132a2d8aa49
130 changes: 120 additions & 10 deletions codex-rs/tui/src/new_model_popup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ use codex_core::config::SWIFTFOX_MEDIUM_MODEL;
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;
Expand All @@ -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")),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit confused on the naming. why are we skipping 4 numbers? cannot we just have 001 ...

Side note, cannot we autogenerate this list instead of hardcoding it?

We can even just load the who directory, or have a incremental value

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is an artifact from the .txt file frame numbers. we might want to maintain file names as an index

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,
Expand All @@ -32,6 +98,8 @@ struct ModelUpgradePopup {
highlighted: ModelUpgradeOption,
decision: Option<ModelUpgradeDecision>,
request_frame: FrameRequester,
frame_idx: usize,
variant_idx: usize,
}

impl ModelUpgradePopup {
Expand All @@ -40,6 +108,8 @@ impl ModelUpgradePopup {
highlighted: ModelUpgradeOption::TryNewModel,
decision: None,
request_frame,
frame_idx: 0,
variant_idx: 0,
}
}

Expand All @@ -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();
}
}
_ => {}
}
}
Expand All @@ -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 {
Expand All @@ -81,18 +180,26 @@ 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(),
// Start with the current animation frame.
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_MEDIUM_MODEL}, 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> {
Expand Down Expand Up @@ -136,13 +243,16 @@ 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() {
if let Some(event) = events.next().await {
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());
});
Expand Down