mod keys;
use super::health_tail::HealthTailer;
use super::lifecycle::cleanup;
use super::registry::NodeRegistry;
use crate::context::CliContext;
use crate::dev::node_selection::NodeToRun;
use crate::dev::termination::{classify_key, TerminationIntent};
use crate::types::ProjectConfig;
use crate::ui::tui_layout::SidebarTui;
use crate::ui::LogBuffer;
use anyhow::Result;
pub struct InteractiveModeConfig<'a> {
pub project_config: &'a ProjectConfig,
pub nodes_to_run: &'a [NodeToRun],
pub project_name: &'a str,
}
pub struct InteractiveModeState {
pub node_registry: NodeRegistry,
pub lifecycle: Option<crate::dev::lifecycle_adapter::CliLifecycleManager>,
pub log_buffer: LogBuffer,
}
pub async fn run_interactive_mode(
ctx: &mut CliContext,
config: InteractiveModeConfig<'_>,
state: &mut InteractiveModeState,
) -> Result<()> {
use crate::ui::DevModeTui;
use crossterm::event::{self, Event};
use crossterm::execute;
use crossterm::terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen};
use ratatui::{backend::CrosstermBackend, Terminal};
use std::io::stdout;
use std::sync::atomic::Ordering;
use std::time::Duration;
let running = crate::dev::termination::install_quit_signal()?;
let redis_url = ctx.redis_url().to_string();
if enable_raw_mode().is_err() {
println!("\n(No interactive terminal — running in headless mode. Press Ctrl+C to stop.)\n");
while running.load(Ordering::SeqCst) {
tokio::time::sleep(Duration::from_millis(200)).await;
}
cleanup(ctx)?;
return Ok(());
}
let mut stdout_handle = stdout();
execute!(stdout_handle, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout_handle);
let mut terminal = Terminal::new(backend)?;
terminal.clear()?;
let current_mode = state
.lifecycle
.as_ref()
.map(|l| l.current_mode().to_string())
.unwrap_or_else(|| "startup".to_string());
let running_nodes: Vec<String> = state.node_registry.running_node_names();
let dashboard_url = config.project_config.environments.dashboard_url();
let mut dev_tui = DevModeTui::new(state.log_buffer.clone(), current_mode, running_nodes, dashboard_url);
let mut health_tailer = HealthTailer::default();
let result: Result<()> = async {
loop {
if !running.load(Ordering::SeqCst) {
return Ok(());
}
health_tailer.poll(&state.node_registry.running_node_names(), &state.log_buffer);
terminal.draw(|f| dev_tui.draw(f))?;
if event::poll(Duration::from_millis(100))? {
if let Event::Key(key_event) = event::read()? {
if let Some(intent) = classify_key(&key_event) {
match intent {
TerminationIntent::QuitAll => {
running.store(false, Ordering::SeqCst);
return Ok(());
}
TerminationIntent::StopCurrent => match key_event.code {
crossterm::event::KeyCode::Char('x') => {
if state.lifecycle.as_ref().map(|l| l.current_mode().to_lowercase())
== Some("simulation".to_string())
{
keys::handle_stop_simulation_key(
ctx,
&config,
state,
&mut terminal,
&mut dev_tui,
)
.await?;
}
}
_ => {
running.store(false, Ordering::SeqCst);
return Ok(());
}
},
}
continue;
}
if let Some(c) = dev_tui.handle_key(key_event) {
match c {
's' => {
keys::handle_simulation_key(
ctx,
&config,
state,
&mut terminal,
&mut dev_tui,
&redis_url,
&running,
)
.await?;
}
't' => {
keys::handle_teleop_key(
ctx,
&config,
state,
&mut terminal,
&mut dev_tui,
&redis_url,
&running,
)
.await?;
}
_ => {}
}
}
}
}
}
}
.await;
disable_raw_mode()?;
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
terminal.show_cursor()?;
cleanup(ctx)?;
result
}