141 lines
4.6 KiB
Rust
141 lines
4.6 KiB
Rust
|
|
// 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(())
|
||
|
|
}
|