// event_loop.rs — TUI event loop // // Drives the terminal, renders the UI, dispatches user input and // hotkey actions to the Mind via a channel. Reads shared state // (agent, active tools) directly for rendering. use anyhow::Result; use crossterm::event::{Event, EventStream, KeyEventKind}; use futures::StreamExt; use std::time::Duration; use crate::user::{self as tui, HotkeyAction}; use crate::user::ui_channel::{self, UiMessage}; /// Messages from the UI to the Mind. pub enum MindMessage { UserInput(String), Hotkey(HotkeyAction), } pub async fn run( mut app: tui::App, mind_tx: tokio::sync::mpsc::UnboundedSender, mut ui_rx: ui_channel::UiReceiver, mut observe_input_rx: tokio::sync::mpsc::UnboundedReceiver, channel_tx: tokio::sync::mpsc::Sender>, mut channel_rx: tokio::sync::mpsc::Receiver>, notify_rx: std::sync::mpsc::Receiver, mut idle_state: crate::thalamus::idle::State, ) -> Result<()> { let mut terminal = tui::init_terminal()?; let mut reader = EventStream::new(); let mut render_interval = tokio::time::interval(Duration::from_millis(50)); render_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); let mut dirty = true; terminal.hide_cursor()?; // Initial render app.drain_messages(&mut ui_rx); terminal.draw(|f| app.draw(f))?; loop { tokio::select! { biased; maybe_event = reader.next() => { match maybe_event { Some(Ok(Event::Key(key))) => { if key.kind != KeyEventKind::Press { continue; } app.handle_key(key); idle_state.user_activity(); if app.screen == tui::Screen::Thalamus { let tx = channel_tx.clone(); tokio::spawn(async move { let result = crate::thalamus::channels::fetch_all_channels().await; let _ = tx.send(result).await; }); } dirty = true; } Some(Ok(Event::Mouse(mouse))) => { app.handle_mouse(mouse); dirty = true; } Some(Ok(Event::Resize(w, h))) => { app.handle_resize(w, h); terminal.clear()?; dirty = true; } Some(Err(_)) => break, None => break, _ => continue, } } Some(line) = observe_input_rx.recv() => { app.submitted.push(line); dirty = true; } _ = render_interval.tick() => { idle_state.decay_ewma(); app.update_idle(&idle_state); while let Ok(notif) = notify_rx.try_recv() { let tx = channel_tx.clone(); tokio::spawn(async move { let result = crate::thalamus::channels::fetch_all_channels().await; let _ = tx.send(result).await; }); } } Some(channels) = channel_rx.recv() => { app.set_channel_status(channels); dirty = true; } Some(msg) = ui_rx.recv() => { app.handle_ui_message(msg); dirty = true; } } // Process submitted input let submitted: Vec = app.submitted.drain(..).collect(); for input in submitted { let input = input.trim().to_string(); if input.is_empty() { continue; } match input.as_str() { "/quit" | "/exit" => app.should_quit = true, _ => { let _ = mind_tx.send(MindMessage::UserInput(input)); } } } // Send hotkey actions to Mind let actions: Vec = app.hotkey_actions.drain(..).collect(); for action in actions { let _ = mind_tx.send(MindMessage::Hotkey(action)); } if app.drain_messages(&mut ui_rx) { dirty = true; } if dirty { terminal.draw(|f| app.draw(f))?; dirty = false; } if app.should_quit { break; } } tui::restore_terminal(&mut terminal)?; Ok(()) }