KeiSeiKit-1.0/_primitives/_rust/kei-tty/src/runner.rs
Parfii-bot 0be354a920 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

93 lines
2.8 KiB
Rust

//! Async event loop — couples the [`App`] state machine to crossterm key
//! events and the daemon SSE stream over a `tokio::mpsc` channel.
use crate::app::{App, LineKind};
use crate::client::chat_stream;
use crate::keys::{handle_key, KeyOutcome};
use crate::types::ChatEvent;
use crate::ui::draw;
use anyhow::Result;
use crossterm::event::{Event, EventStream, KeyEventKind};
use futures::StreamExt;
use ratatui::backend::Backend;
use ratatui::Terminal;
use tokio::sync::mpsc;
/// Run the TUI event loop until the user presses Ctrl+D / Ctrl+C twice.
pub async fn run<B: Backend>(
terminal: &mut Terminal<B>,
daemon_url: String,
token: String,
user_id: String,
) -> Result<()> {
let mut app = App::new();
let (tx, mut rx) = mpsc::unbounded_channel::<ChatEvent>();
let mut keys = EventStream::new();
while !app.should_quit {
terminal.draw(|f| draw(f, &app))?;
tokio::select! {
maybe_key = keys.next() => {
if let Some(Ok(Event::Key(k))) = maybe_key {
if k.kind != KeyEventKind::Release {
dispatch_key(&mut app, k, &daemon_url, &token, &user_id, tx.clone());
}
}
}
Some(ev) = rx.recv() => {
app.apply_event(ev);
}
}
}
Ok(())
}
/// Hand a [`KeyEvent`](crossterm::event::KeyEvent) to [`handle_key`] and
/// react to the resulting [`KeyOutcome`].
fn dispatch_key(
app: &mut App,
k: crossterm::event::KeyEvent,
daemon_url: &str,
token: &str,
user_id: &str,
tx: mpsc::UnboundedSender<ChatEvent>,
) {
match handle_key(k, app) {
KeyOutcome::Send(msg) => start_send(app, msg, daemon_url, token, user_id, tx),
KeyOutcome::Quit => app.should_quit = true,
KeyOutcome::Cancel => {
app.cancel_requested = true;
app.in_flight = false;
app.status = "cancelled".into();
}
KeyOutcome::Nothing => {}
}
}
/// Spawn the background daemon-client task for a single send.
fn start_send(
app: &mut App,
msg: String,
daemon_url: &str,
token: &str,
user_id: &str,
tx: mpsc::UnboundedSender<ChatEvent>,
) {
app.push_line(LineKind::User, msg.clone());
app.in_flight = true;
app.status = "streaming…".into();
let url = daemon_url.to_string();
let token = token.to_string();
let uid = user_id.to_string();
let cid = app.conversation_id.clone();
tokio::spawn(async move {
let send = |e: ChatEvent| {
let _ = tx.send(e);
};
if let Err(e) = chat_stream(&url, &token, &uid, &msg, cid, send).await {
let _ = tx.clone().send(ChatEvent::Error(e.to_string()));
let _ = tx.send(ChatEvent::Done {
conversation_id: String::new(),
});
}
});
}