An AI agent built to do Ralph loops - plan mode for planning and ralph mode for implementing.
1mod app;
2pub mod messages;
3mod ui;
4pub mod views;
5pub mod widgets;
6
7pub use messages::{AgentMessage, AgentReceiver, AgentSender, agent_channel};
8
9pub use app::{ActiveTab, App};
10pub use ui::draw;
11
12use crossterm::{
13 event::{self, Event, KeyEventKind},
14 execute,
15 terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
16};
17use ratatui::{Terminal, backend::CrosstermBackend};
18use std::io;
19use std::panic;
20use std::time::Duration;
21use tokio::sync::mpsc::Receiver;
22
23pub type Tui = Terminal<CrosstermBackend<io::Stdout>>;
24
25pub fn setup_terminal() -> io::Result<Tui> {
26 enable_raw_mode()?;
27 let mut stdout = io::stdout();
28 execute!(stdout, EnterAlternateScreen)?;
29 let backend = CrosstermBackend::new(stdout);
30 Terminal::new(backend)
31}
32
33pub fn restore_terminal(terminal: &mut Tui) -> io::Result<()> {
34 disable_raw_mode()?;
35 execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
36 terminal.show_cursor()?;
37 Ok(())
38}
39
40/// Run the TUI event loop with panic safety.
41pub async fn run(
42 terminal: &mut Tui,
43 app: &mut App,
44 agent_rx: &mut Receiver<AgentMessage>,
45) -> anyhow::Result<()> {
46 let original_hook = panic::take_hook();
47 panic::set_hook(Box::new(|info| {
48 let _ = disable_raw_mode();
49 let _ = execute!(io::stdout(), LeaveAlternateScreen);
50 eprintln!("Panic: {}", info);
51 }));
52
53 let result = run_loop(terminal, app, agent_rx).await;
54
55 // Restore original panic hook
56 let _ = panic::take_hook();
57 panic::set_hook(original_hook);
58
59 result
60}
61
62async fn run_loop(
63 terminal: &mut Tui,
64 app: &mut App,
65 agent_rx: &mut Receiver<AgentMessage>,
66) -> anyhow::Result<()> {
67 let mut interval = tokio::time::interval(Duration::from_millis(50));
68
69 loop {
70 if !app.running {
71 break;
72 }
73
74 terminal.draw(|frame| crate::tui::ui::draw(frame, app))?;
75
76 tokio::select! {
77 _ = interval.tick() => {
78 app.spinner.tick();
79
80 while event::poll(Duration::ZERO)? {
81 match event::read()? {
82 Event::Key(key) if key.kind == KeyEventKind::Press => {
83 app.handle_key(key);
84 }
85 Event::Resize(_, _) => {
86 // Redraw handled next iteration
87 }
88 _ => {}
89 }
90 }
91 }
92
93 Some(msg) = agent_rx.recv() => {
94 app.handle_agent_message(msg);
95 }
96 }
97 }
98 Ok(())
99}