use std::io;
use std::time::Duration;
use crossterm::event::{Event, EventStream, KeyCode, KeyModifiers};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
use crossterm::execute;
use futures::StreamExt;
use ratatui::prelude::*;
use tokio::sync::mpsc;
use super::{App, AppEvent, Focus, Screen};
use super::ui;
use crate::cli::error::Result;
use crate::runtime::AgentRuntime;
use crate::transport::Transport;
pub async fn run_tui(
mut app: App,
mut event_rx: mpsc::UnboundedReceiver<AppEvent>,
runtime: AgentRuntime,
transport: Box<dyn Transport>,
) -> Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut runtime_handle = tokio::spawn(async move {
runtime.run(transport).await
});
let mut event_stream = EventStream::new();
let mut tick = tokio::time::interval(Duration::from_millis(250));
let result = loop {
terminal.draw(|f| ui::render(f, &mut app))?;
tokio::select! {
maybe_event = event_stream.next() => {
match maybe_event {
Some(Ok(Event::Key(key))) => {
if handle_key(&mut app, key) {
break Ok(());
}
}
Some(Err(e)) => {
break Err(crate::cli::error::CliError::Io(e));
}
_ => {}
}
}
Some(event) = event_rx.recv() => {
app.update(event);
}
_ = tick.tick() => {}
result = &mut runtime_handle => {
match result {
Ok(inner) => break inner,
Err(e) => break Err(crate::cli::error::CliError::Other(format!("runtime panic: {}", e))),
}
}
}
};
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
result
}
fn normalize_key(code: KeyCode) -> KeyCode {
match code {
KeyCode::Char(c) => KeyCode::Char(c.to_ascii_lowercase()),
other => other,
}
}
fn handle_key(app: &mut App, key: crossterm::event::KeyEvent) -> bool {
if key.modifiers.contains(KeyModifiers::CONTROL) && key.code == KeyCode::Char('c') {
return true;
}
let code = normalize_key(key.code);
match &app.screen {
Screen::Main => match code {
KeyCode::Char('q') => return true,
KeyCode::Up | KeyCode::Char('k') => match app.focus {
Focus::Table => app.select_prev(),
Focus::Log => app.log_scroll = app.log_scroll.saturating_sub(1),
},
KeyCode::Down | KeyCode::Char('j') => match app.focus {
Focus::Table => app.select_next(),
Focus::Log => app.log_scroll = app.log_scroll.saturating_add(1),
},
KeyCode::Enter => {
if let Some(idx) = app.table_state.selected() {
if idx < app.jobs.len() {
app.detail_scroll = 0;
app.screen = Screen::JobDetail(idx);
}
}
}
KeyCode::Tab => {
app.focus = match app.focus {
Focus::Table => Focus::Log,
Focus::Log => Focus::Table,
};
}
KeyCode::Char('s') => {
app.toggle_sound();
}
KeyCode::Char('r') => {
app.refresh_recovery();
app.recovery_detail_scroll = 0;
if app.recovery_table_state.selected().is_none() && !app.recovery_entries.is_empty() {
app.recovery_table_state.select(Some(0));
}
app.screen = Screen::Recovery;
}
_ => {}
},
Screen::JobDetail(_) => match code {
KeyCode::Char('q') => return true,
KeyCode::Esc => {
app.screen = Screen::Main;
}
KeyCode::Up | KeyCode::Char('k') => {
app.detail_scroll = app.detail_scroll.saturating_sub(1);
}
KeyCode::Down | KeyCode::Char('j') => {
app.detail_scroll = app.detail_scroll.saturating_add(1);
}
_ => {}
},
Screen::Recovery => match code {
KeyCode::Char('q') => return true,
KeyCode::Esc => {
app.screen = Screen::Main;
}
KeyCode::Char('r') => {
app.refresh_recovery();
}
KeyCode::Enter => {
app.retry_selected();
}
KeyCode::Up | KeyCode::Char('k') => {
let len = app.recovery_entries.len();
if len > 0 {
let i = app.recovery_table_state.selected()
.map(|i| if i == 0 { len - 1 } else { i - 1 })
.unwrap_or(0);
app.recovery_table_state.select(Some(i));
}
}
KeyCode::Down | KeyCode::Char('j') => {
let len = app.recovery_entries.len();
if len > 0 {
let i = app.recovery_table_state.selected()
.map(|i| if i + 1 >= len { 0 } else { i + 1 })
.unwrap_or(0);
app.recovery_table_state.select(Some(i));
}
}
_ => {}
},
}
false
}