Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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
85 changes: 85 additions & 0 deletions codex-rs/tui-pty-e2e/tests/nori_banner.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
//! E2E tests for the Nori welcome banner display.
//!
//! These tests verify that the Nori ASCII art banner is displayed
//! correctly during startup.

use insta::assert_snapshot;
use tui_pty_e2e::normalize_for_snapshot;
use tui_pty_e2e::SessionConfig;
use tui_pty_e2e::TuiSession;
use tui_pty_e2e::TIMEOUT;
use tui_pty_e2e::TIMEOUT_INPUT;

#[test]
fn test_startup_shows_nori_banner() {
let mut session = TuiSession::spawn_with_config(
24,
80,
SessionConfig::default()
// Don't include the values that would bypass welcome
.without_approval_policy()
.without_sandbox()
.with_config_toml(""),
)
.expect("Failed to spawn codex");

// Wait for the Nori ASCII art to appear
session
.wait_for_text("|_| \\_|", TIMEOUT)
.expect("Nori ASCII art banner did not appear");
std::thread::sleep(TIMEOUT_INPUT);

let contents = session.screen_contents();

// Verify Nori ASCII art is present (distinctive part of the logo)
assert!(
contents.contains("|_| \\_|"),
"Expected Nori ASCII art banner, but got: {}",
contents
);

// Verify the tagline is present
assert!(
contents.contains("powered by Nori"),
"Expected Nori tagline, but got: {}",
contents
);

assert_snapshot!(
"startup_nori_banner",
normalize_for_snapshot(session.screen_contents())
);
}

#[test]
fn test_nori_banner_shows_profile() {
let mut session = TuiSession::spawn_with_config(
24,
80,
SessionConfig::default()
.without_approval_policy()
.without_sandbox()
.with_config_toml(""),
)
.expect("Failed to spawn codex");

// Wait for the Nori banner to appear
session
.wait_for_text("|_| \\_|", TIMEOUT)
.expect("Nori ASCII art banner did not appear");
std::thread::sleep(TIMEOUT_INPUT);

let contents = session.screen_contents();

// Verify profile line is displayed
assert!(
contents.contains("profile:"),
"Expected profile line in banner, but got: {}",
contents
);

assert_snapshot!(
"nori_banner_profile",
normalize_for_snapshot(session.screen_contents())
);
}
2 changes: 2 additions & 0 deletions codex-rs/tui/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ mod markdown;
mod markdown_render;
mod markdown_stream;
mod model_migration;
mod nori_banner;
pub mod onboarding;
mod oss_selection;
mod pager_overlay;
Expand Down Expand Up @@ -92,6 +93,7 @@ use crate::onboarding::onboarding_screen::run_onboarding_app;
use crate::tui::Tui;
pub use cli::Cli;
pub use markdown_render::render_markdown_text;
pub use nori_banner::NoriBanner;
pub use public_widgets::composer_input::ComposerAction;
pub use public_widgets::composer_input::ComposerInput;
use std::io::Write as _;
Expand Down
142 changes: 142 additions & 0 deletions codex-rs/tui/src/nori_banner.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
//! Nori welcome banner widget with ASCII art and status line.
//!
//! This module provides a self-contained banner widget that displays:
//! - Green ANSI-colored ASCII art for "NORI"
//! - A status line with profile name and tagline

use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::text::Line;
use ratatui::text::Span;
use ratatui::widgets::Paragraph;
use ratatui::widgets::Widget;
use ratatui::widgets::WidgetRef;

/// ASCII art for "NORI" logo
const NORI_ASCII_ART: &[&str] = &[
r" _ _ ___ ____ ___ ",
r"| \ | |/ _ \| _ \|_ _|",
r"| \| | | | | |_) || | ",
r"| |\ | |_| | _ < | | ",
r"|_| \_|\___/|_| \_\___|",
];

/// A welcome banner widget displaying the Nori ASCII art and status line.
pub struct NoriBanner {
profile: String,
}

impl NoriBanner {
/// Creates a new NoriBanner with the given profile name.
pub fn new(profile: impl Into<String>) -> Self {
Self {
profile: profile.into(),
}
}

/// Renders the banner lines with styling applied.
fn render_lines(&self) -> Vec<Line<'static>> {
let mut lines: Vec<Line<'static>> = Vec::new();

// Add ASCII art lines in green
for art_line in NORI_ASCII_ART {
lines.push(Line::from(Span::styled(
art_line.to_string(),
ratatui::style::Style::default().fg(Color::Green),
)));
}

// Add empty line for spacing
lines.push(Line::from(""));

// Add profile line
lines.push(Line::from(format!("profile: {}", self.profile)));

// Add tagline
lines.push(Line::from("🍙 powered by Nori 🍙"));

lines
}
}

impl WidgetRef for &NoriBanner {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let lines = self.render_lines();
Paragraph::new(lines).render(area, buf);
}
}

#[cfg(all(test, feature = "vt100-tests"))]
mod tests {
use super::*;
use crate::test_backend::VT100Backend;
use ratatui::Terminal;

#[test]
fn nori_banner_renders_ascii_art() {
let banner = NoriBanner::new("clifford");
let mut terminal =
Terminal::new(VT100Backend::new(80, 12)).expect("Failed to create terminal");
terminal
.draw(|f| (&banner).render_ref(f.area(), f.buffer_mut()))
.expect("Failed to draw");

let contents = terminal.backend().to_string();

// Verify ASCII art is present (check for distinctive parts of the art)
assert!(
contents.contains(r"|_| \_|"),
"Banner should contain ASCII art for NORI logo"
);
assert!(
contents.contains(r"| \ | |"),
"Banner should contain ASCII art characters"
);

insta::assert_snapshot!("nori_banner_ascii_art", terminal.backend().to_string());
}

#[test]
fn nori_banner_shows_profile_and_tagline() {
let banner = NoriBanner::new("test-profile");
let mut terminal =
Terminal::new(VT100Backend::new(80, 12)).expect("Failed to create terminal");
terminal
.draw(|f| (&banner).render_ref(f.area(), f.buffer_mut()))
.expect("Failed to draw");

let contents = terminal.backend().to_string();

assert!(
contents.contains("profile: test-profile"),
"Banner should show profile name"
);
assert!(
contents.contains("🍙 powered by Nori 🍙"),
"Banner should show tagline"
);

insta::assert_snapshot!(
"nori_banner_profile_tagline",
terminal.backend().to_string()
);
}

#[test]
fn nori_banner_uses_green_color() {
let banner = NoriBanner::new("clifford");
let lines = banner.render_lines();

// Check that the first line (ASCII art) has green styling
let first_line = &lines[0];
assert!(!first_line.spans.is_empty(), "First line should have spans");

let first_span = &first_line.spans[0];
assert_eq!(
first_span.style.fg,
Some(Color::Green),
"ASCII art should be styled with green color"
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
source: tui/src/nori_banner.rs
expression: terminal.backend().to_string()
---
_ _ ___ ____ ___
| \ | |/ _ \| _ \|_ _|
| \| | | | | |_) || |
| |\ | |_| | _ < | |
|_| \_|\___/|_| \_\___|

profile: clifford
🍙 powered by Nori 🍙
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
source: tui/src/nori_banner.rs
expression: terminal.backend().to_string()
---
_ _ ___ ____ ___
| \ | |/ _ \| _ \|_ _|
| \| | | | | |_) || |
| |\ | |_| | _ < | |
|_| \_|\___/|_| \_\___|

profile: test-profile
🍙 powered by Nori 🍙
Loading