use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::mpsc as std_mpsc;
use tokio::sync::mpsc::UnboundedReceiver;
use tokio::time::{Interval, interval};
use crate::error::Result;
use crate::repl::SteerBuffer;
use crate::repl::tui::app::{self, App, Picker};
use crate::repl::tui::event::{Job, UiEvent};
use crate::repl::tui::input::{handle_idle_key, handle_picker_key};
use crate::repl::tui::keymap::handle_confirmation_key;
use crate::repl::tui::{MAX_OUTPUT_BATCH, TICK_INTERVAL, inline_tui, ui};
pub(super) async fn event_loop(
tui: &mut inline_tui::InlineTui,
app: &mut App,
mut ui_rx: UnboundedReceiver<UiEvent>,
job_tx: std_mpsc::Sender<Job>,
interrupt: Arc<AtomicBool>,
steer_buffer: SteerBuffer,
) -> Result<()> {
let mut tick: Interval = interval(TICK_INTERVAL);
let mut last_size: (u16, u16) = (0, 0);
render_frame(tui, app, &mut last_size)?;
loop {
let event = tokio::select! {
ev = ui_rx.recv() => ev,
_ = tick.tick() => Some(UiEvent::Tick),
};
let Some(first) = event else {
break;
};
let mut current = first;
loop {
match current {
UiEvent::Tick => {
if app.busy() && app.confirmation.is_none() {
app.advance_spinner();
}
break;
}
UiEvent::Output { kind: _, text } => {
if crossterm::terminal::size()? != last_size {
render_frame(tui, app, &mut last_size)?;
}
let mut batch: Vec<String> = Vec::with_capacity(32);
batch.push(text);
let mut forwarded: Option<UiEvent> = None;
while batch.len() < MAX_OUTPUT_BATCH {
match ui_rx.try_recv() {
Ok(UiEvent::Output { text, .. }) => batch.push(text),
Ok(other) => {
forwarded = Some(other);
break;
}
Err(_) => break,
}
}
tui.queue_history_lines(batch);
if let Some(next) = forwarded {
current = next;
continue;
}
break;
}
UiEvent::Key(key) => {
if app.confirmation.is_some() {
handle_confirmation_key(app, key);
} else if app.picker.is_some() {
handle_picker_key(app, key, &job_tx);
} else {
handle_idle_key(app, key, &job_tx, &interrupt, &steer_buffer);
}
break;
}
UiEvent::Paste(text) => {
if app.confirmation.is_none() && app.picker.is_none() {
app.textarea.insert_str(text);
}
break;
}
UiEvent::Resize => {
break;
}
UiEvent::WorkerBusy(label) => {
app.start_busy(label);
break;
}
UiEvent::WorkerIdle => {
app.finish_busy();
if app.picker.is_none() {
let residual: Vec<String> = std::mem::take(
&mut *steer_buffer
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner()),
);
if !residual.is_empty() {
let _ = job_tx.send(Job::Message {
text: residual.join("\n\n"),
images: Vec::new(),
});
} else if let Some(next) = app.queue.pop_front() {
let _ = job_tx.send(next);
}
}
break;
}
UiEvent::Status(snapshot) => {
app.status = Some(snapshot);
break;
}
UiEvent::ShowResumePicker(sessions) => {
app.picker = Some(Picker {
sessions,
cursor: 0,
});
break;
}
UiEvent::ConfirmRequest {
prompt,
choices,
default_index,
kind,
responder,
} => {
let initial_cursor =
if matches!(kind, crate::tools::utils::ConfirmationType::Permission) {
0
} else {
default_index.min(choices.len().saturating_sub(1))
};
app.confirmation = Some(app::ConfirmationPrompt {
prompt,
cursor: initial_cursor,
default_index,
choices,
kind,
responder,
});
break;
}
UiEvent::WorkerShutdown(summary) => {
app.exit_summary = Some(summary);
app.should_quit = true;
let mut pending_batch: Vec<String> = Vec::new();
while let Ok(pending) = ui_rx.try_recv() {
if let UiEvent::Output { text, .. } = pending {
pending_batch.push(text);
}
}
if !pending_batch.is_empty() {
tui.queue_history_lines(pending_batch);
}
break;
}
}
}
render_frame(tui, app, &mut last_size)?;
if app.should_quit {
break;
}
}
Ok(())
}
fn render_frame(
tui: &mut inline_tui::InlineTui,
app: &mut App,
last_size: &mut (u16, u16),
) -> Result<()> {
let current_size = crossterm::terminal::size()?;
let desired_height = ui::desired_viewport_height(app, current_size.0);
tui.draw(desired_height, |f| ui::draw(f, app))?;
*last_size = current_size;
Ok(())
}