use std::time::{Duration, Instant}; use crossterm::{ event::{self, Event, KeyCode}, execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, }; use ratatui::{ backend::CrosstermBackend, Terminal, layout::{Constraint, Direction, Layout}, style::{Color, Style}, text::{Line, Span}, widgets::{Block, Borders, List, ListItem, Paragraph}, }; use crate::state::{SharedState, ChromiumPhase, HeadlessPhase}; pub fn start_ui(state: SharedState) -> std::io::Result<()> { enable_raw_mode()?; let mut stdout = std::io::stdout(); execute!(stdout, EnterAlternateScreen)?; let backend = CrosstermBackend::new(stdout); let mut terminal = Terminal::new(backend)?; let mut last_tick = Instant::now(); let tick_rate = Duration::from_millis(250); loop { let snapshot = { let s = state.lock().unwrap(); s.clone() }; terminal.draw(|f| { let chunks = Layout::default() .direction(Direction::Vertical) .margin(1) .constraints([ Constraint::Length(4), Constraint::Min(6), Constraint::Min(6), ]) .split(f.size()); let header = Block::default() .title(" Aurora Core — Engines ") .borders(Borders::ALL); let web_dot = Span::styled( "●", Style::default().fg(if snapshot.web_server_active { Color::Green } else { Color::Red }) ); let plug_dot = Span::styled( "●", Style::default().fg(if snapshot.plugin_engine_active { Color::Green } else { Color::Red }) ); // Chromium Punkt + optionaler Text let (chrom_dot_color, chrom_text) = match &snapshot.chromium { ChromiumPhase::Ready => (Color::Green, "".to_string()), ChromiumPhase::Downloading => (Color::Yellow, "downloading…".to_string()), ChromiumPhase::Checking => (Color::Yellow, "checking…".to_string()), ChromiumPhase::Idle => (Color::Red, "".to_string()), ChromiumPhase::Error(msg) => (Color::Red, format!("error: {}", msg)), }; let chrom_dot = Span::styled("●", Style::default().fg(chrom_dot_color)); // Headless Engine Punkt + optionaler Text let (head_dot_color, head_text) = match &snapshot.headless { HeadlessPhase::Ready => (Color::Green, "".to_string()), HeadlessPhase::Starting => (Color::Yellow, "starting…".to_string()), HeadlessPhase::Idle => (Color::Red, "".to_string()), HeadlessPhase::Error(msg) => (Color::Red, format!("error: {}", msg)), }; let head_dot = Span::styled("●", Style::default().fg(head_dot_color)); let mut spans = vec![ Span::raw("Web Server "), web_dot, Span::raw(" "), Span::raw("Plugin Engine "), plug_dot, Span::raw(" "), Span::raw("Chromium "), chrom_dot, ]; if !chrom_text.is_empty() { spans.push(Span::raw(" ")); spans.push(Span::styled(format!("({})", chrom_text), Style::default().fg(Color::Gray))); } spans.push(Span::raw(" ")); spans.push(Span::raw("Headless Engine ")); spans.push(head_dot); if !head_text.is_empty() { spans.push(Span::raw(" ")); spans.push(Span::styled(format!("({})", head_text), Style::default().fg(Color::Gray))); } let header_para = Paragraph::new(Line::from(spans)).block(header); f.render_widget(header_para, chunks[0]); // Loaded plugins let plugin_block = Block::default().title(" Loaded Plugins ").borders(Borders::ALL); let items: Vec = if snapshot.loaded_plugins.is_empty() { vec![ListItem::new("— none —")] } else { snapshot.loaded_plugins.iter().map(|p| ListItem::new(p.clone())).collect() }; f.render_widget(List::new(items).block(plugin_block), chunks[1]); // Running jobs let jobs_block = Block::default().title(" Active Setups ").borders(Borders::ALL); let jobs_items: Vec = if snapshot.running_jobs.is_empty() { vec![ListItem::new("— idle —")] } else { snapshot.running_jobs.iter().map(|j| ListItem::new(j.clone())).collect() }; f.render_widget(List::new(jobs_items).block(jobs_block), chunks[2]); })?; let timeout = tick_rate.checked_sub(last_tick.elapsed()).unwrap_or(Duration::from_secs(0)); if crossterm::event::poll(timeout)? { if let Event::Key(key) = event::read()? { match key.code { KeyCode::Char('q') | KeyCode::Esc => break, _ => {} } } } if last_tick.elapsed() >= tick_rate { last_tick = Instant::now(); } } disable_raw_mode()?; let mut stdout = std::io::stdout(); execute!(stdout, LeaveAlternateScreen)?; Ok(()) }