consciousness/src/user/event_loop.rs

141 lines
4.6 KiB
Rust
Raw Normal View History

// 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<MindMessage>,
mut ui_rx: ui_channel::UiReceiver,
mut observe_input_rx: tokio::sync::mpsc::UnboundedReceiver<String>,
channel_tx: tokio::sync::mpsc::Sender<Vec<(String, bool, u32)>>,
mut channel_rx: tokio::sync::mpsc::Receiver<Vec<(String, bool, u32)>>,
notify_rx: std::sync::mpsc::Receiver<crate::thalamus::channels::ChannelNotification>,
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<String> = 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<HotkeyAction> = 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(())
}