use std::time::Duration;
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use tokio::sync::broadcast;
use crate::event::EventKind;
use crate::provider::rig::StreamChunk;
use super::super::views::{TuiView, View, ViewAction};
use super::types::Action;
use super::App;
use crate::tui::InputMode;
impl App {
pub(crate) fn poll_runtime_events(&mut self) {
self.event_buffer.clear();
if let Some(ref mut rx) = self.broadcast_rx {
loop {
match rx.try_recv() {
Ok(event) => self.event_buffer.push(event),
Err(broadcast::error::TryRecvError::Empty) => break,
Err(broadcast::error::TryRecvError::Lagged(n)) => {
tracing::warn!("TUI lagged behind by {} events", n);
}
Err(broadcast::error::TryRecvError::Closed) => {
self.workflow_done = true;
break;
}
}
}
}
if let Some(ref mut rx) = self.event_rx {
while let Ok(event) = rx.try_recv() {
self.event_buffer.push(event);
}
}
let events: Vec<_> = self.event_buffer.drain(..).collect();
for event in events {
match &event.kind {
EventKind::WorkflowCompleted { .. } => {
self.workflow_done = true;
}
EventKind::WorkflowFailed { .. } => {
self.workflow_done = true;
}
_ => {}
}
self.state.handle_event(&event.kind, event.timestamp_ms);
}
self.poll_llm_responses();
self.poll_stream_chunks();
}
fn poll_llm_responses(&mut self) {
while let Ok(response) = self.llm_response_rx.try_recv() {
if let Some(last) = self.command_view.chat.messages.last() {
if last.content == "Thinking..." || last.content.starts_with("$ ") {
self.command_view.chat.messages.pop();
}
}
self.command_view.chat.add_nika_message(response, None);
}
}
fn poll_stream_chunks(&mut self) {
while let Ok(chunk) = self.stream_chunk_rx.try_recv() {
match chunk {
StreamChunk::Token(token) => {
if !self.command_view.chat.is_streaming {
use crate::tui::widgets::DecryptVerb;
self.command_view
.chat
.start_streaming_with_verb(DecryptVerb::Infer);
}
self.command_view.chat.append_streaming(&token);
}
StreamChunk::Done(_) => {
self.command_view.chat.finalize_thinking();
}
StreamChunk::Error(err) => {
if self.command_view.chat.is_streaming {
self.command_view.chat.finish_streaming();
}
self.command_view.chat.show_error(&err);
}
StreamChunk::Thinking(text) => {
self.command_view.chat.append_thinking(&text);
}
_ => {}
}
}
}
pub(crate) fn handle_unified_key(&mut self, code: KeyCode, modifiers: KeyModifiers) -> Action {
if let (KeyCode::Char('c'), KeyModifiers::CONTROL) = (code, modifiers) {
return self.handle_ctrl_c();
}
if code == KeyCode::Char('q')
&& modifiers.is_empty()
&& self.input_mode == InputMode::Normal
{
return Action::Quit;
}
if code == KeyCode::Esc && self.input_mode != InputMode::Normal {
self.input_mode = InputMode::Normal;
return Action::Continue;
}
let alt_pressed = modifiers.contains(KeyModifiers::ALT);
let is_normal = self.input_mode == InputMode::Normal;
let on_control = self.current_view == TuiView::Control;
let on_command = self.current_view == TuiView::Command;
if alt_pressed || (is_normal && modifiers.is_empty() && !on_control) {
if let Some(view) = match code {
KeyCode::Char('1') => Some(TuiView::Studio),
KeyCode::Char('2') => Some(TuiView::Command),
KeyCode::Char('3') => Some(TuiView::Control),
_ => None,
} {
return Action::SwitchView(view);
}
}
if is_normal && modifiers.is_empty() && !on_command {
if let Some(view) = match code {
KeyCode::Char('s') => Some(TuiView::Studio),
KeyCode::Char('c') => Some(TuiView::Command),
KeyCode::Char('x') => Some(TuiView::Control),
_ => None,
} {
return Action::SwitchView(view);
}
}
let view_action = self.dispatch_to_current_view(code, modifiers);
Action::from_view_action(view_action)
}
fn dispatch_to_current_view(&mut self, code: KeyCode, modifiers: KeyModifiers) -> ViewAction {
let key = KeyEvent::new(code, modifiers);
match self.current_view {
TuiView::Studio => self.studio_view.handle_key(key, &mut self.state),
TuiView::Command => self.command_view.handle_key(key, &mut self.state),
TuiView::Control => self.control_view.handle_key(key, &mut self.state),
}
}
fn handle_ctrl_c(&mut self) -> Action {
let now = std::time::Instant::now();
if let Some(last) = self.last_ctrl_c {
if now.duration_since(last) < Duration::from_millis(500) {
return Action::Quit;
}
}
self.last_ctrl_c = Some(now);
self.set_status("Press Ctrl+C again to quit");
Action::Continue
}
}