diff --git a/codex-rs/tui/frames/default/frame_10.txt b/codex-rs/tui/frames/default/frame_10.txt index ffe56a1e..9d454173 100644 --- a/codex-rs/tui/frames/default/frame_10.txt +++ b/codex-rs/tui/frames/default/frame_10.txt @@ -3,14 +3,14 @@              *'`+*+\~/_*,                          ^_,||/~~-~+\,,                        |__/\|;_.\,''\\,            -           / ;||"|^ ',/_/|/            -          |` '|*~//\  !`_"|            +           / ;||"|^  /_/|/            +          |` '|*~//\   `_"|                      \  ~*"*||~|*   |/,           -          "  ||\+/+||-_`.\||           +          "  ||\+/+||-_ .\||                     "  ~\ \\|;~~+\+;||                     |  ,|\,|_/_*___|*`                      , "|||||""!\,"\|`           -           \`',\,*" _~"",//            +           \`',\,*"  "",//                        |' |||~*,:,/|/`                         ;`**/|+;_!//'                           *, _*\_,;*               diff --git a/codex-rs/tui/frames/default/frame_16.txt b/codex-rs/tui/frames/default/frame_16.txt index 75c9353f..7217fe58 100644 --- a/codex-rs/tui/frames/default/frame_16.txt +++ b/codex-rs/tui/frames/default/frame_16.txt @@ -2,15 +2,15 @@                 _=+"**+~_                             /^||*||\|=\                           |//"\/=\|| '\            -             /// ;*_\|\||**|           -             ||;_/*'=||*/|`\           -            |||*|' |\= !| ~.|          -            \\| /`,||||/*", |          +             /// ;' \|\||**|           +             ||;_ =||*/|`\           +            |||*|  /|= !| ~.|          +            \\|  ,||||/*", |                      |/; |`||/|||"; `|                      \\|~|+~/^||"*+  /                      *"__,==\*|._| ,_|                      |||+""/*\|;";.~|`          -             ||* |`  `//,  /           +             ||* |   `//,  /                        \|*  |  /,/_,|                          \|~"_*~//+_|                            ':._=:__;*              diff --git a/codex-rs/tui/frames/default/frame_17.txt b/codex-rs/tui/frames/default/frame_17.txt index 9d7a2dc3..0d873df7 100644 --- a/codex-rs/tui/frames/default/frame_17.txt +++ b/codex-rs/tui/frames/default/frame_17.txt @@ -2,11 +2,11 @@                 ,=+++;;~,_                          _;**|~~*=*|,"^,                       ,*\/_==`+,"|||_"\          -           /|/_/|"_` '|;\~||=\         -           |/_ ~    |"/\=\//  ,        +           /|/_/|"   |;\~||=\         +           |/_ ~     "/\=\//  ,                  `=*,/`   ,:/| /,=/|./                  *!;/|   ,//|_ *"||/=|        -          -"=|!   !//||/|,||=;*        +          -"=|!   !//||/ ,||=;*                  ,/*/\==+~\_|\^:\||| |                   |"_;__|/*\/||\!\+'+\                  \\\/"""****\_|*//\ \'        diff --git a/codex-rs/tui/frames/default/frame_18.txt b/codex-rs/tui/frames/default/frame_18.txt index 123e46cd..a474a4f3 100644 --- a/codex-rs/tui/frames/default/frame_18.txt +++ b/codex-rs/tui/frames/default/frame_18.txt @@ -1,10 +1,10 @@                                                       _==+==+;~,_                         _+"_;,++~__,"+;;_          -          _/:|*"*=","._"+//\ *         -         ,||*.,^"* '\`_/=~\;\\\,       -        _\// /|  _"+_\/~/|_\;\\_       -        /\| ,,~.___/,*,|'-/^/`/~!      +          _/:|*"*=" "._"+//\ *         +         ,||*.,^" _/=~\;\\\,       +        _\// /|   _\/~/|_\;\\_       +        /\| ,, _/,*,|'-/^/`/~!              ||\:/     +/*/|"_/"*|=|=,              "\-~|     ^\"||;^   |;|"              \"" ,\==;=;+~|,|*/\, |*|`       diff --git a/codex-rs/tui/frames/default/frame_8.txt b/codex-rs/tui/frames/default/frame_8.txt index efe7c1b0..2e8019c0 100644 --- a/codex-rs/tui/frames/default/frame_8.txt +++ b/codex-rs/tui/frames/default/frame_8.txt @@ -5,7 +5,7 @@         _*\*/|,+|     ' =^||\                  ' /|/,\|/\      .'\||,                |',|\^^_\|_*      \+|||         -       |*||| '_;\|`|     |^|/|,        +       |*||| '_;\|`|      ^|/|,               \,||/  |+\/|,*.    .`/||               \ \||_^ !/*=|~/+,+,,\~||               !  \*/=_|",|,||;|=__||='        diff --git a/codex-rs/tui/frames/default/frame_9.txt b/codex-rs/tui/frames/default/frame_9.txt index 072d0eef..128e9150 100644 --- a/codex-rs/tui/frames/default/frame_9.txt +++ b/codex-rs/tui/frames/default/frame_9.txt @@ -3,10 +3,10 @@             /*_/||*"=!_-\_                        / ,||/*^^=/!\_~,                      " ||/;=_ _^,|\^|+,           -         /*";\*"|*, *.'+:+||           -         | |||"^|\;__ |'|*|||          -         ` ~*\|**|\\,".,/ `||          -        |  ~/_,\~||_/=\_| !||          +         /*";\*"|*,  +:+||           +         | |||"^|\;*   '|*|||          +         ` ~*\ **|\\," / `||          +        |  ~/_ ~||_/= | !||                  !  ",|" /|"|~~|+|~,||`                  |_|||_,|^|_||||__|||                   " `^/\\|/"****||"/\|          diff --git a/codex-rs/tui/src/frames.rs b/codex-rs/tui/src/frames.rs new file mode 100644 index 00000000..19a70578 --- /dev/null +++ b/codex-rs/tui/src/frames.rs @@ -0,0 +1,71 @@ +use std::time::Duration; + +// Embed animation frames for each variant at compile time. +macro_rules! frames_for { + ($dir:literal) => { + [ + include_str!(concat!("../frames/", $dir, "/frame_1.txt")), + include_str!(concat!("../frames/", $dir, "/frame_2.txt")), + include_str!(concat!("../frames/", $dir, "/frame_3.txt")), + include_str!(concat!("../frames/", $dir, "/frame_4.txt")), + include_str!(concat!("../frames/", $dir, "/frame_5.txt")), + include_str!(concat!("../frames/", $dir, "/frame_6.txt")), + include_str!(concat!("../frames/", $dir, "/frame_7.txt")), + include_str!(concat!("../frames/", $dir, "/frame_8.txt")), + include_str!(concat!("../frames/", $dir, "/frame_9.txt")), + include_str!(concat!("../frames/", $dir, "/frame_10.txt")), + include_str!(concat!("../frames/", $dir, "/frame_11.txt")), + include_str!(concat!("../frames/", $dir, "/frame_12.txt")), + include_str!(concat!("../frames/", $dir, "/frame_13.txt")), + include_str!(concat!("../frames/", $dir, "/frame_14.txt")), + include_str!(concat!("../frames/", $dir, "/frame_15.txt")), + include_str!(concat!("../frames/", $dir, "/frame_16.txt")), + include_str!(concat!("../frames/", $dir, "/frame_17.txt")), + include_str!(concat!("../frames/", $dir, "/frame_18.txt")), + include_str!(concat!("../frames/", $dir, "/frame_19.txt")), + include_str!(concat!("../frames/", $dir, "/frame_20.txt")), + include_str!(concat!("../frames/", $dir, "/frame_21.txt")), + include_str!(concat!("../frames/", $dir, "/frame_22.txt")), + include_str!(concat!("../frames/", $dir, "/frame_23.txt")), + include_str!(concat!("../frames/", $dir, "/frame_24.txt")), + include_str!(concat!("../frames/", $dir, "/frame_25.txt")), + include_str!(concat!("../frames/", $dir, "/frame_26.txt")), + include_str!(concat!("../frames/", $dir, "/frame_27.txt")), + include_str!(concat!("../frames/", $dir, "/frame_28.txt")), + include_str!(concat!("../frames/", $dir, "/frame_29.txt")), + include_str!(concat!("../frames/", $dir, "/frame_30.txt")), + include_str!(concat!("../frames/", $dir, "/frame_31.txt")), + include_str!(concat!("../frames/", $dir, "/frame_32.txt")), + include_str!(concat!("../frames/", $dir, "/frame_33.txt")), + include_str!(concat!("../frames/", $dir, "/frame_34.txt")), + include_str!(concat!("../frames/", $dir, "/frame_35.txt")), + include_str!(concat!("../frames/", $dir, "/frame_36.txt")), + ] + }; +} + +pub(crate) const FRAMES_DEFAULT: [&str; 36] = frames_for!("default"); +pub(crate) const FRAMES_CODEX: [&str; 36] = frames_for!("codex"); +pub(crate) const FRAMES_OPENAI: [&str; 36] = frames_for!("openai"); +pub(crate) const FRAMES_BLOCKS: [&str; 36] = frames_for!("blocks"); +pub(crate) const FRAMES_DOTS: [&str; 36] = frames_for!("dots"); +pub(crate) const FRAMES_HASH: [&str; 36] = frames_for!("hash"); +pub(crate) const FRAMES_HBARS: [&str; 36] = frames_for!("hbars"); +pub(crate) const FRAMES_VBARS: [&str; 36] = frames_for!("vbars"); +pub(crate) const FRAMES_SHAPES: [&str; 36] = frames_for!("shapes"); +pub(crate) const FRAMES_SLUG: [&str; 36] = frames_for!("slug"); + +pub(crate) const ALL_VARIANTS: &[&[&str]] = &[ + &FRAMES_DEFAULT, + &FRAMES_CODEX, + &FRAMES_OPENAI, + &FRAMES_BLOCKS, + &FRAMES_DOTS, + &FRAMES_HASH, + &FRAMES_HBARS, + &FRAMES_VBARS, + &FRAMES_SHAPES, + &FRAMES_SLUG, +]; + +pub(crate) const FRAME_TICK_DEFAULT: Duration = Duration::from_millis(80); diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs index 27faef18..5fe36dd1 100644 --- a/codex-rs/tui/src/lib.rs +++ b/codex-rs/tui/src/lib.rs @@ -42,6 +42,7 @@ pub mod custom_terminal; mod diff_render; mod exec_command; mod file_search; +mod frames; mod get_git_diff; mod history_cell; pub mod insert_history; diff --git a/codex-rs/tui/src/new_model_popup.rs b/codex-rs/tui/src/new_model_popup.rs index b5b63d3c..153a91a2 100644 --- a/codex-rs/tui/src/new_model_popup.rs +++ b/codex-rs/tui/src/new_model_popup.rs @@ -1,3 +1,5 @@ +use crate::frames::ALL_VARIANTS as FRAME_VARIANTS; +use crate::frames::FRAME_TICK_DEFAULT; use crate::tui::FrameRequester; use crate::tui::Tui; use crate::tui::TuiEvent; @@ -19,75 +21,7 @@ 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_1.txt")), - include_str!(concat!("../frames/", $dir, "/frame_2.txt")), - include_str!(concat!("../frames/", $dir, "/frame_3.txt")), - include_str!(concat!("../frames/", $dir, "/frame_4.txt")), - include_str!(concat!("../frames/", $dir, "/frame_5.txt")), - include_str!(concat!("../frames/", $dir, "/frame_6.txt")), - include_str!(concat!("../frames/", $dir, "/frame_7.txt")), - include_str!(concat!("../frames/", $dir, "/frame_8.txt")), - include_str!(concat!("../frames/", $dir, "/frame_9.txt")), - include_str!(concat!("../frames/", $dir, "/frame_10.txt")), - include_str!(concat!("../frames/", $dir, "/frame_11.txt")), - include_str!(concat!("../frames/", $dir, "/frame_12.txt")), - include_str!(concat!("../frames/", $dir, "/frame_13.txt")), - include_str!(concat!("../frames/", $dir, "/frame_14.txt")), - include_str!(concat!("../frames/", $dir, "/frame_15.txt")), - include_str!(concat!("../frames/", $dir, "/frame_16.txt")), - include_str!(concat!("../frames/", $dir, "/frame_17.txt")), - include_str!(concat!("../frames/", $dir, "/frame_18.txt")), - include_str!(concat!("../frames/", $dir, "/frame_19.txt")), - include_str!(concat!("../frames/", $dir, "/frame_20.txt")), - include_str!(concat!("../frames/", $dir, "/frame_21.txt")), - include_str!(concat!("../frames/", $dir, "/frame_22.txt")), - include_str!(concat!("../frames/", $dir, "/frame_23.txt")), - include_str!(concat!("../frames/", $dir, "/frame_24.txt")), - include_str!(concat!("../frames/", $dir, "/frame_25.txt")), - include_str!(concat!("../frames/", $dir, "/frame_26.txt")), - include_str!(concat!("../frames/", $dir, "/frame_27.txt")), - include_str!(concat!("../frames/", $dir, "/frame_28.txt")), - include_str!(concat!("../frames/", $dir, "/frame_29.txt")), - include_str!(concat!("../frames/", $dir, "/frame_30.txt")), - include_str!(concat!("../frames/", $dir, "/frame_31.txt")), - include_str!(concat!("../frames/", $dir, "/frame_32.txt")), - include_str!(concat!("../frames/", $dir, "/frame_33.txt")), - include_str!(concat!("../frames/", $dir, "/frame_34.txt")), - include_str!(concat!("../frames/", $dir, "/frame_35.txt")), - include_str!(concat!("../frames/", $dir, "/frame_36.txt")), - ] - }; -} - -const FRAMES_DEFAULT: [&str; 36] = frames_for!("default"); -const FRAMES_CODEX: [&str; 36] = frames_for!("codex"); -const FRAMES_OPENAI: [&str; 36] = frames_for!("openai"); -const FRAMES_BLOCKS: [&str; 36] = frames_for!("blocks"); -const FRAMES_DOTS: [&str; 36] = frames_for!("dots"); -const FRAMES_HASH: [&str; 36] = frames_for!("hash"); -const FRAMES_HBARS: [&str; 36] = frames_for!("hbars"); -const FRAMES_VBARS: [&str; 36] = frames_for!("vbars"); -const FRAMES_SHAPES: [&str; 36] = frames_for!("shapes"); -const FRAMES_SLUG: [&str; 36] = 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); +const FRAME_TICK: Duration = FRAME_TICK_DEFAULT; #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum ModelUpgradeDecision { @@ -156,11 +90,11 @@ impl ModelUpgradePopup { } fn frames(&self) -> &'static [&'static str] { - VARIANTS[self.variant_idx] + FRAME_VARIANTS[self.variant_idx] } fn pick_random_variant(&mut self) { - let total = VARIANTS.len(); + let total = FRAME_VARIANTS.len(); if total <= 1 { return; } @@ -196,13 +130,11 @@ impl WidgetRef for &ModelUpgradePopup { lines.push("".into()); lines.push( - format!( - " Codex is now powered by {SWIFTFOX_MODEL_DISPLAY_NAME}, a new model that is" - ) - .into(), + format!(" Codex is now powered by {SWIFTFOX_MODEL_DISPLAY_NAME}, a new model that is") + .into(), ); lines.push(Line::from(vec![ - " ".into(), + " ".into(), "faster, a better collaborator, ".bold(), "and ".into(), "more steerable.".bold(), @@ -226,6 +158,7 @@ impl WidgetRef for &ModelUpgradePopup { ModelUpgradeOption::TryNewModel, &format!("Yes, switch me to {SWIFTFOX_MODEL_DISPLAY_NAME}"), )); + lines.push("".into()); lines.push(create_option( 1, ModelUpgradeOption::KeepCurrent, diff --git a/codex-rs/tui/src/onboarding/auth.rs b/codex-rs/tui/src/onboarding/auth.rs index d419bada..2ea59a31 100644 --- a/codex-rs/tui/src/onboarding/auth.rs +++ b/codex-rs/tui/src/onboarding/auth.rs @@ -137,12 +137,12 @@ impl AuthModeWidget { fn render_pick_mode(&self, area: Rect, buf: &mut Buffer) { let mut lines: Vec = vec![ Line::from(vec![ - "> ".into(), - "Sign in with ChatGPT to use Codex as part of your paid plan".bold(), + " ".into(), + "Sign in with ChatGPT to use Codex as part of your paid plan".into(), ]), Line::from(vec![ " ".into(), - "or connect an API key for usage-based billing".bold(), + "or connect an API key for usage-based billing".into(), ]), "".into(), ]; @@ -182,6 +182,7 @@ impl AuthModeWidget { "Sign in with ChatGPT", "Usage included with Plus, Pro, and Team plans", )); + lines.push("".into()); lines.extend(create_mode_item( 1, AuthMode::ApiKey, @@ -205,7 +206,7 @@ impl AuthModeWidget { } fn render_continue_in_browser(&self, area: Rect, buf: &mut Buffer) { - let mut spans = vec!["> ".into()]; + let mut spans = vec![" ".into()]; // Schedule a follow-up frame to keep the shimmer animation going. self.request_frame .schedule_frame_in(std::time::Duration::from_millis(100)); @@ -217,7 +218,8 @@ impl AuthModeWidget { && !state.auth_url.is_empty() { lines.push(" If the link doesn't open automatically, open the following link to authenticate:".into()); - lines.push(vec![" ".into(), state.auth_url.as_str().cyan().underlined()].into()); + lines.push("".into()); + lines.push(Line::from(state.auth_url.as_str().cyan().underlined())); lines.push("".into()); } @@ -231,7 +233,7 @@ impl AuthModeWidget { let lines = vec![ "✓ Signed in with your ChatGPT account".fg(Color::Green).into(), "".into(), - "> Before you start:".into(), + " Before you start:".into(), "".into(), " Decide how much autonomy you want to grant Codex".into(), Line::from(vec![ diff --git a/codex-rs/tui/src/onboarding/onboarding_screen.rs b/codex-rs/tui/src/onboarding/onboarding_screen.rs index c9617d79..76e533b4 100644 --- a/codex-rs/tui/src/onboarding/onboarding_screen.rs +++ b/codex-rs/tui/src/onboarding/onboarding_screen.rs @@ -24,6 +24,7 @@ use crate::tui::TuiEvent; use color_eyre::eyre::Result; use std::sync::Arc; use std::sync::RwLock; +use std::time::Instant; #[allow(clippy::large_enum_variant)] enum Step { @@ -74,6 +75,8 @@ impl OnboardingScreen { let codex_home = config.codex_home; let mut steps: Vec = vec![Step::Welcome(WelcomeWidget { is_logged_in: !matches!(login_status, LoginStatus::NotAuthenticated), + request_frame: tui.frame_requester(), + start: Instant::now(), })]; if show_login_screen { steps.push(Step::Auth(AuthModeWidget { diff --git a/codex-rs/tui/src/onboarding/welcome.rs b/codex-rs/tui/src/onboarding/welcome.rs index 5e6906bf..40911b84 100644 --- a/codex-rs/tui/src/onboarding/welcome.rs +++ b/codex-rs/tui/src/onboarding/welcome.rs @@ -3,23 +3,62 @@ use ratatui::layout::Rect; use ratatui::prelude::Widget; use ratatui::style::Stylize; use ratatui::text::Line; +use ratatui::widgets::Paragraph; use ratatui::widgets::WidgetRef; +use ratatui::widgets::Wrap; +use crate::frames::FRAME_TICK_DEFAULT; +use crate::frames::FRAMES_DEFAULT; use crate::onboarding::onboarding_screen::StepStateProvider; +use crate::tui::FrameRequester; use super::onboarding_screen::StepState; +use std::time::Duration; +use std::time::Instant; + +const FRAME_TICK: Duration = FRAME_TICK_DEFAULT; pub(crate) struct WelcomeWidget { pub is_logged_in: bool, + pub request_frame: FrameRequester, + pub start: Instant, } impl WidgetRef for &WelcomeWidget { fn render_ref(&self, area: Rect, buf: &mut Buffer) { - let line = Line::from(vec![ - ">_ ".into(), - "Welcome to Codex, OpenAI's command-line coding agent".bold(), - ]); - line.render(area, buf); + let elapsed_ms = self.start.elapsed().as_millis(); + + // Align next draw to the next FRAME_TICK boundary to reduce jitter. + { + let tick_ms = FRAME_TICK.as_millis(); + let rem_ms = elapsed_ms % tick_ms; + let delay_ms = if rem_ms == 0 { + tick_ms + } else { + tick_ms - rem_ms + }; + // Safe cast: delay_ms < tick_ms and FRAME_TICK is small. + self.request_frame + .schedule_frame_in(Duration::from_millis(delay_ms as u64)); + } + + let frames = &FRAMES_DEFAULT; + let idx = ((elapsed_ms / FRAME_TICK.as_millis()) % frames.len() as u128) as usize; + + let mut lines: Vec = Vec::with_capacity(frames.len() + 2); + lines.extend(frames[idx].lines().map(|l| l.into())); + + lines.push("".into()); + lines.push(Line::from(vec![ + " ".into(), + "Welcome to ".into(), + "Codex".bold(), + ", OpenAI's command-line coding agent".into(), + ])); + + Paragraph::new(lines) + .wrap(Wrap { trim: false }) + .render(area, buf); } } @@ -31,3 +70,14 @@ impl StepStateProvider for WelcomeWidget { } } } + +#[cfg(test)] +mod tests { + use super::*; + + /// A number of things break down if FRAME_TICK is zero. + #[test] + fn frame_tick_must_be_nonzero() { + assert!(FRAME_TICK.as_millis() > 0); + } +}