mirror of
https://github.com/openai/codex.git
synced 2026-11-13 22:47:20 +00:00
Introduce a full codex-tui source snapshot under the new codex-tui2 crate so viewport work can be replayed in isolation. This change copies the entire codex-rs/tui/src tree into codex-rs/tui2/src in one atomic step, rather than piecemeal, to keep future diffs vs the original viewport bookmark easy to reason about. The goal is for codex-tui2 to render identically to the existing TUI behind the `features.tui2` flag while we gradually port the viewport/history commits from the joshka/viewport bookmark onto this forked tree. While on this baseline change, we also ran the codex-tui2 snapshot test suite and accepted all insta snapshots for the new crate, so the snapshot files now use the codex-tui2 naming scheme and encode the unmodified legacy TUI behavior. This keeps later viewport commits focused on intentional behavior changes (and their snapshots) rather than on mechanical snapshot renames.
154 lines
4.6 KiB
Rust
154 lines
4.6 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 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 = 20;
|
|
const MIN_ANIMATION_WIDTH: u16 = 60;
|
|
|
|
pub(crate) struct WelcomeWidget {
|
|
pub is_logged_in: bool,
|
|
animation: AsciiAnimation,
|
|
animations_enabled: bool,
|
|
}
|
|
|
|
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,
|
|
}
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
// Skip the animation entirely when the viewport is too small so we don't clip frames.
|
|
let show_animation =
|
|
area.height >= MIN_ANIMATION_HEIGHT && area.width >= MIN_ANIMATION_WIDTH;
|
|
|
|
let mut lines: Vec<Line> = Vec::new();
|
|
if show_animation && self.animations_enabled {
|
|
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 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];
|
|
|
|
#[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);
|
|
(&widget).render(area, &mut buf);
|
|
|
|
let mut found = false;
|
|
let mut last_non_empty: Option<u16> = None;
|
|
for y in 0..area.height {
|
|
for x in 0..area.width {
|
|
if !buf[(x, y)].symbol().trim().is_empty() {
|
|
found = true;
|
|
last_non_empty = Some(y);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
assert!(found, "expected welcome animation to render characters");
|
|
let measured_rows = last_non_empty.map(|v| v + 2).unwrap_or(0);
|
|
assert!(
|
|
measured_rows >= MIN_ANIMATION_HEIGHT,
|
|
"expected measurement to report at least {MIN_ANIMATION_HEIGHT} rows, got {measured_rows}"
|
|
);
|
|
}
|
|
|
|
#[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,
|
|
};
|
|
|
|
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"
|
|
);
|
|
}
|
|
}
|