use std::io;
use std::time::Duration;
use ratatui::Terminal;
use ratatui::backend::CrosstermBackend;
use ratatui::crossterm::SynchronizedUpdate;
use ratatui::crossterm::event::Event as TerminalEvent;
use ratatui::crossterm::event::KeyEventKind;
use ratatui::crossterm::execute;
use ratatui::crossterm::style::Print;
use tokio::sync::mpsc;
use tokio::time::MissedTickBehavior;
use super::TranscriptTone;
use super::TuiState;
use super::events::handle_event;
use super::input::UiAction;
use super::view::render_preview;
use crate::frontend::FrontendExit;
use crate::frontend::catalog::UiCatalog;
use crate::frontend::terminal::{INPUT_POLL, MAX_INPUT_BATCH, TerminalGuard, poll_event};
use horus::Result;
use horus::agent::Agent;
use horus::protocol::Event;
use horus::protocol::EventMsg;
use horus::protocol::FrontendBlock;
use horus::protocol::Op;
const ELAPSED_INTERVAL: Duration = Duration::from_secs(1);
const MAX_EVENT_BATCH: usize = 64;
const CLEAR_SCREEN_AND_SCROLLBACK: &str = "\x1b[r\x1b[0m\x1b[H\x1b[2J\x1b[3J\x1b[H";
pub(in crate::frontend) async fn run(agent: Agent, catalog: UiCatalog) -> Result<FrontendExit> {
let mut workspace_inventory = catalog.start_workspace_inventory();
let mut workspace_inventory_pending = true;
let frontend = agent.frontend().clone();
let render = |event: &EventMsg| frontend.render(event);
let model = agent.model().clone();
let model_route = agent.model_route().to_string();
let mut state = TuiState::new(&catalog, std::env::current_dir()?, model, model_route);
let (sender, mut events) = agent.into_parts();
let guard = TerminalGuard::alternate()?;
let mut terminal = Terminal::new(CrosstermBackend::new(io::stdout()))?;
terminal.clear()?;
let mut tick = tokio::time::interval(INPUT_POLL);
tick.set_missed_tick_behavior(MissedTickBehavior::Skip);
let mut elapsed = tokio::time::interval(ELAPSED_INTERVAL);
elapsed.set_missed_tick_behavior(MissedTickBehavior::Skip);
let mut events_open = true;
let mut dirty = true;
let mut exit = FrontendExit::Exit;
let mut clear_on_exit = false;
'ui: loop {
if dirty {
io::stdout().sync_update(|_| -> Result<()> {
terminal.draw(|frame| {
if state.preview.is_some() {
render_preview(frame, &mut state);
} else {
super::view::render(frame, &mut state, &catalog);
}
})?;
Ok(())
})??;
dirty = false;
}
tokio::select! {
event = events.recv(), if events_open => {
match event {
Some(event) => {
handle_event_batch(&mut state, &render, event, &mut events);
if let Some(session_id) = state.requested_resume.take() {
clear_on_exit = true;
exit = FrontendExit::Resume(session_id);
break 'ui;
}
}
None => {
events_open = false;
state.disconnected = true;
state.finish_turn();
state.push("agent disconnected ยท press q to exit", TranscriptTone::Error);
}
}
dirty = true;
}
_ = tick.tick() => {
for _ in 0..MAX_INPUT_BATCH {
let Some(event) = poll_event()? else {
break;
};
let action = match event {
TerminalEvent::Key(key) => {
dirty |= matches!(key.kind, KeyEventKind::Press | KeyEventKind::Repeat);
state.handle_key(key, &catalog)
}
TerminalEvent::Paste(text) => {
if state.preview.is_none() && state.picker.is_none() {
let before = (state.input.len(), state.input_limit_reached);
state.insert_paste(&text);
dirty |=
before != (state.input.len(), state.input_limit_reached);
}
UiAction::None
}
TerminalEvent::Resize(_, _) => {
dirty = true;
UiAction::None
}
TerminalEvent::Mouse(mouse) => {
dirty |= state.handle_mouse(mouse);
UiAction::None
}
TerminalEvent::FocusGained
| TerminalEvent::FocusLost => UiAction::None,
};
match action {
UiAction::None => {}
UiAction::Exit => {
if let Some(turn_id) = state.active_turn.clone() {
let _ = sender.submit(Op::Interrupt { turn_id });
}
break 'ui;
}
UiAction::New(model_route) => {
exit = FrontendExit::New(model_route);
break 'ui;
}
UiAction::Clear(model_route) => {
clear_on_exit = true;
exit = FrontendExit::New(model_route);
break 'ui;
}
UiAction::Setup => {
exit = FrontendExit::Setup;
break 'ui;
}
UiAction::Submit(op) => {
if let Err(error) = sender.submit(op) {
state.push(error.to_string(), TranscriptTone::Error);
}
}
}
}
}
_ = elapsed.tick(), if state.active_turn.is_some() => {
dirty = true;
}
result = &mut workspace_inventory, if workspace_inventory_pending => {
let _ = result;
workspace_inventory_pending = false;
state.reference_cache = None;
dirty = true;
}
}
}
drop(terminal);
drop(guard);
if clear_on_exit {
execute!(io::stdout(), Print(CLEAR_SCREEN_AND_SCROLLBACK))?;
}
Ok(exit)
}
fn handle_event_batch<R>(
state: &mut TuiState,
render: &R,
first: Event,
events: &mut mpsc::Receiver<Event>,
) where
R: Fn(&EventMsg) -> Vec<FrontendBlock>,
{
handle_event(state, render, first.msg);
for _ in 1..MAX_EVENT_BATCH {
if state.requested_resume.is_some() {
break;
}
let Ok(event) = events.try_recv() else {
break;
};
handle_event(state, render, event.msg);
}
}