Files
codex/codex-rs/tui/src/onboarding/welcome.rs
T
Matthew Zeng 182000999c Raise welcome animation breakpoint to 37 rows (#9778)
### Motivation
- The large ASCII welcome animation can push onboarding content below
the fold on default-height terminals, making the CLI appear
unresponsive; raising the breakpoint prevents that.
- The existing test measured an arbitrary row count rather than
asserting the welcome line position relative to the animation frame,
which made the intent unclear.

### Description
- Increase `MIN_ANIMATION_HEIGHT` from `20` to `37` in
`codex-rs/tui/src/onboarding/welcome.rs` so the animation is skipped
unless there is enough vertical space.
- Replace the brittle measurement logic in the welcome render test with
a `row_containing` helper and assert the welcome row equals the frame
height plus the spacer line (`frame_lines + 1`).
- Add a regression test
`welcome_skips_animation_below_height_breakpoint` that verifies the
animation is not rendered when the viewport height is one row below the
breakpoint.

### Testing
- Ran formatting with `~/.cargo/bin/just fmt` which completed
successfully.
- Ran unit tests for the crate with `cargo test -p codex-tui --lib` and
they passed (unit test suite succeeded).
- Ran `cargo test -p codex-tui` which reported a failing integration
test in this environment because the test cannot locate the `codex`
binary, so full crate tests are blocked here (environment limitation).

------
[Codex
Task](https://chatgpt.com/codex/tasks/task_i_6973b0a710d4832c9ff36fac26eb1519)
2026-01-24 21:50:35 -08:00

171 lines
5.2 KiB
Rust

use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
use crossterm::event::KeyEventKind;
use crossterm::event::KeyModifiers;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::prelude::Widget;
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::widgets::Clear;
use ratatui::widgets::Paragraph;
use ratatui::widgets::WidgetRef;
use ratatui::widgets::Wrap;
use std::cell::Cell;
use crate::ascii_animation::AsciiAnimation;
use crate::onboarding::onboarding_screen::KeyboardHandler;
use crate::onboarding::onboarding_screen::StepStateProvider;
use crate::tui::FrameRequester;
use super::onboarding_screen::StepState;
const MIN_ANIMATION_HEIGHT: u16 = 37;
const MIN_ANIMATION_WIDTH: u16 = 60;
pub(crate) struct WelcomeWidget {
pub is_logged_in: bool,
animation: AsciiAnimation,
animations_enabled: bool,
layout_area: Cell<Option<Rect>>,
}
impl KeyboardHandler for WelcomeWidget {
fn handle_key_event(&mut self, key_event: KeyEvent) {
if !self.animations_enabled {
return;
}
if key_event.kind == KeyEventKind::Press
&& key_event.code == KeyCode::Char('.')
&& key_event.modifiers.contains(KeyModifiers::CONTROL)
{
tracing::warn!("Welcome background to press '.'");
let _ = self.animation.pick_random_variant();
}
}
}
impl WelcomeWidget {
pub(crate) fn new(
is_logged_in: bool,
request_frame: FrameRequester,
animations_enabled: bool,
) -> Self {
Self {
is_logged_in,
animation: AsciiAnimation::new(request_frame),
animations_enabled,
layout_area: Cell::new(None),
}
}
pub(crate) fn update_layout_area(&self, area: Rect) {
self.layout_area.set(Some(area));
}
}
impl WidgetRef for &WelcomeWidget {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
Clear.render(area, buf);
if self.animations_enabled {
self.animation.schedule_next_frame();
}
let layout_area = self.layout_area.get().unwrap_or(area);
// Skip the animation entirely when the viewport is too small so we don't clip frames.
let show_animation = self.animations_enabled
&& layout_area.height >= MIN_ANIMATION_HEIGHT
&& layout_area.width >= MIN_ANIMATION_WIDTH;
let mut lines: Vec<Line> = Vec::new();
if show_animation {
let frame = self.animation.current_frame();
lines.extend(frame.lines().map(Into::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);
}
}
impl StepStateProvider for WelcomeWidget {
fn get_step_state(&self) -> StepState {
match self.is_logged_in {
true => StepState::Hidden,
false => StepState::Complete,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
static VARIANT_A: [&str; 1] = ["frame-a"];
static VARIANT_B: [&str; 1] = ["frame-b"];
static VARIANTS: [&[&str]; 2] = [&VARIANT_A, &VARIANT_B];
fn row_containing(buf: &Buffer, needle: &str) -> Option<u16> {
(0..buf.area.height).find(|&y| {
let mut row = String::new();
for x in 0..buf.area.width {
row.push_str(buf[(x, y)].symbol());
}
row.contains(needle)
})
}
#[test]
fn welcome_renders_animation_on_first_draw() {
let widget = WelcomeWidget::new(false, FrameRequester::test_dummy(), true);
let area = Rect::new(0, 0, MIN_ANIMATION_WIDTH, MIN_ANIMATION_HEIGHT);
let mut buf = Buffer::empty(area);
let frame_lines = widget.animation.current_frame().lines().count() as u16;
(&widget).render(area, &mut buf);
let welcome_row = row_containing(&buf, "Welcome");
assert_eq!(welcome_row, Some(frame_lines + 1));
}
#[test]
fn welcome_skips_animation_below_height_breakpoint() {
let widget = WelcomeWidget::new(false, FrameRequester::test_dummy(), true);
let area = Rect::new(0, 0, MIN_ANIMATION_WIDTH, MIN_ANIMATION_HEIGHT - 1);
let mut buf = Buffer::empty(area);
(&widget).render(area, &mut buf);
let welcome_row = row_containing(&buf, "Welcome");
assert_eq!(welcome_row, Some(0));
}
#[test]
fn ctrl_dot_changes_animation_variant() {
let mut widget = WelcomeWidget {
is_logged_in: false,
animation: AsciiAnimation::with_variants(FrameRequester::test_dummy(), &VARIANTS, 0),
animations_enabled: true,
layout_area: Cell::new(None),
};
let before = widget.animation.current_frame();
widget.handle_key_event(KeyEvent::new(KeyCode::Char('.'), KeyModifiers::CONTROL));
let after = widget.animation.current_frame();
assert_ne!(
before, after,
"expected ctrl+. to switch welcome animation variant"
);
}
}