mod agent;
mod app;
mod history;
mod ui;
use std::io;
use std::time::Duration;
use crossterm::event;
use ratatui::backend::CrosstermBackend;
use ratatui::{Terminal, TerminalOptions, Viewport};
use tuika::components::Flex;
use tuika::probe::RectProbe;
use tuika::screen::{ScreenMode, close_footer, pin_footer, publish_block};
use tuika::{
Dimension, Element, Padding, RenderCtx, StyleSheet, TerminalSession, Theme, element, paint,
translate_event,
};
use crate::app::{App, Flow};
use crate::history::Cell;
const FOOTER_ROWS: u16 = tuika::screen::DEFAULT_FOOTER_HEIGHT;
pub const FRAME_MS: u64 = 60;
fn main() -> io::Result<()> {
let args: Vec<String> = std::env::args().skip(1).collect();
match args.first().map(String::as_str) {
Some("--help" | "-h") => {
println!("A replica of the Codex CLI's interface, built on tuika.");
println!("Not the Codex CLI; not affiliated with OpenAI. The agent is scripted:");
println!("no model, no network, no shell.");
println!();
println!("usage: cargo run --example codex [-- --split-footer | --dump [scene]]");
println!("scenes: welcome, turn, approval, slash, mention, status");
Ok(())
}
Some("--dump") => ui::dump(args.get(1).map(String::as_str)),
Some("--split-footer") => run_split(),
_ => run(),
}
}
fn run_split() -> io::Result<()> {
let mut app = App::new();
let theme = app::codex_theme();
let sheet = StyleSheet::from_theme(&theme);
let probe = RectProbe::new();
let mode = ScreenMode::split_footer(FOOTER_ROWS);
let _session = TerminalSession::enter_with(mode)?;
let mut terminal = Terminal::with_options(
CrosstermBackend::new(io::stdout()),
TerminalOptions {
viewport: mode.viewport(),
},
)?;
loop {
let width = terminal.get_frame().area().width;
let ctx = RenderCtx::new(&theme).with_sheet(sheet);
for mut cell in app.drain_settled() {
let view = published(&mut cell, width, &theme, &sheet);
publish_block(&mut terminal, view.as_ref(), &ctx)?;
}
terminal.autoresize()?;
pin_footer(&mut terminal)?;
terminal.draw(|f| {
let area = f.area();
let root = ui::build(&mut app, area, &theme, &sheet, &probe);
paint(f.buffer_mut(), area, &theme, root.as_ref(), &[]);
if let Some(pos) = app.cursor(probe.rect()) {
f.set_cursor_position(pos);
}
})?;
if event::poll(Duration::from_millis(FRAME_MS))?
&& let Some(ev) = translate_event(event::read()?)
&& app.handle(&ev) == Flow::Quit
{
break;
}
app.tick();
}
let _ = close_footer(&mut terminal);
drop(terminal);
Ok(())
}
fn published(cell: &mut Cell, width: u16, theme: &Theme, sheet: &StyleSheet) -> Element {
let inner = width.saturating_sub(ui::GUTTER * 2);
element(
Flex::column()
.padding(Padding {
left: ui::GUTTER,
right: ui::GUTTER,
top: 1,
bottom: 0,
})
.child(Dimension::Auto, cell.view(inner, theme, sheet)),
)
}
fn run() -> io::Result<()> {
let mut app = App::new();
let theme = app::codex_theme();
let sheet = StyleSheet::from_theme(&theme);
let probe = RectProbe::new();
let _session = TerminalSession::enter()?;
let mut terminal = Terminal::with_options(
CrosstermBackend::new(io::stdout()),
TerminalOptions {
viewport: Viewport::Fullscreen,
},
)?;
loop {
terminal.draw(|f| {
let area = f.area();
let root = ui::build(&mut app, area, &theme, &sheet, &probe);
paint(f.buffer_mut(), area, &theme, root.as_ref(), &[]);
if let Some(pos) = app.cursor(probe.rect()) {
f.set_cursor_position(pos);
}
})?;
if event::poll(Duration::from_millis(FRAME_MS))?
&& let Some(ev) = translate_event(event::read()?)
&& app.handle(&ev) == Flow::Quit
{
break;
}
app.tick();
}
let _ = terminal.clear();
drop(terminal);
Ok(())
}