mod app;
mod event;
mod input;
mod mouse;
mod session;
mod types;
mod ui;
use app::App;
use event::apply_event;
use types::{AppEvent, CliType, Focus, MAX_PTY_EVENTS_PER_FRAME};
use crossterm::event::{Event, KeyEventKind};
use std::time::Duration;
use tokio::sync::mpsc;
fn process_frame(
app: &mut App,
key_rx: &mut mpsc::UnboundedReceiver<Event>,
rx: &mut mpsc::UnboundedReceiver<AppEvent>,
terminal: &mut ratatui::DefaultTerminal,
) -> std::io::Result<()> {
while let Ok(ev) = key_rx.try_recv() {
match ev {
Event::Key(key) => app.handle_key(key),
Event::Mouse(mouse) => app.handle_mouse(mouse),
_ => {}
}
if app.should_quit {
break;
}
}
for _ in 0..MAX_PTY_EVENTS_PER_FRAME {
match rx.try_recv() {
Ok(ev) => apply_event(app, ev),
Err(_) => break,
}
}
if !app.should_quit {
app.poll_status_files();
terminal.draw(|frame| app.render(frame))?;
}
Ok(())
}
#[tokio::main]
async fn main() -> std::io::Result<()> {
let mut terminal = ratatui::init();
let result = run(&mut terminal).await;
let _ = crossterm::execute!(std::io::stdout(), crossterm::event::DisableMouseCapture);
ratatui::restore();
result
}
async fn run(terminal: &mut ratatui::DefaultTerminal) -> std::io::Result<()> {
crossterm::execute!(std::io::stdout(), crossterm::event::EnableMouseCapture)?;
if let Ok(cwd) = std::env::current_dir() {
let mut components = cwd.components().rev();
let folder = components
.next()
.map(|c| c.as_os_str().to_string_lossy().to_string());
let parent = components
.next()
.map(|c| c.as_os_str().to_string_lossy().to_string());
let title = match (parent, folder) {
(Some(p), Some(f)) => format!("{}/{}", p, f),
(None, Some(f)) => f,
_ => "neimar".to_string(),
};
let _ = crossterm::execute!(
std::io::stdout(),
crossterm::terminal::SetTitle(&title)
);
}
let (key_tx, mut key_rx) = mpsc::unbounded_channel::<Event>();
let (tx, mut rx) = mpsc::unbounded_channel::<AppEvent>();
std::thread::spawn(move || {
loop {
match crossterm::event::read() {
Ok(Event::Key(key))
if key.kind == KeyEventKind::Press || key.kind == KeyEventKind::Repeat =>
{
if key_tx.send(Event::Key(key)).is_err() {
break;
}
}
Ok(Event::Mouse(mouse)) => {
if key_tx.send(Event::Mouse(mouse)).is_err() {
break;
}
}
Ok(_) => {}
Err(_) => break,
}
}
});
let mut app = App::new(tx);
terminal.draw(|frame| app.render(frame))?;
let (rows, cols) = if app.layout.last_right_panel_size.0 > 0 {
app.layout.last_right_panel_size
} else {
(24, 80)
};
app.create_session("claude skip-perm".to_string(), CliType::ClaudeDangerous, rows, cols);
app.create_session("claude".to_string(), CliType::Claude, rows, cols);
app.create_session("console".to_string(), CliType::Console, rows, cols);
app.list_state.select(Some(0));
app.ui.focus = Focus::Terminal;
let mut render_interval = tokio::time::interval(Duration::from_millis(33));
render_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
tokio::select! {
biased;
Some(ev) = key_rx.recv() => {
match ev {
Event::Key(key) => app.handle_key(key),
Event::Mouse(mouse) => app.handle_mouse(mouse),
_ => {}
}
process_frame(&mut app, &mut key_rx, &mut rx, terminal)?;
render_interval.reset();
}
_ = render_interval.tick() => {
process_frame(&mut app, &mut key_rx, &mut rx, terminal)?;
}
}
if app.should_quit {
break;
}
}
app.shutdown();
Ok(())
}