pub mod rich;
pub mod state;
pub mod widgets;
use crate::coordinator::client::CoordinatorClient;
use crossterm::event::{Event as CrosstermEvent, EventStream, KeyCode, KeyModifiers};
use crossterm::terminal::{disable_raw_mode, enable_raw_mode};
use crossterm::{
ExecutableCommand,
cursor::{Hide, Show},
};
use cuenv_events::CuenvEvent;
use futures::StreamExt;
use ratatui::{
Terminal,
backend::CrosstermBackend,
layout::{Alignment, Constraint, Direction, Layout},
style::{Color, Modifier, Style},
widgets::{Block, Borders, List, ListItem, Paragraph},
};
use std::io;
use std::time::Duration;
struct TerminalGuard;
impl Drop for TerminalGuard {
fn drop(&mut self) {
let _ = disable_raw_mode();
let _ = io::stdout().execute(crossterm::terminal::LeaveAlternateScreen);
let _ = io::stdout().execute(Show);
}
}
pub async fn run_event_viewer(client: &mut CoordinatorClient) -> io::Result<()> {
enable_raw_mode()?;
let _guard = TerminalGuard;
let mut stdout = io::stdout();
stdout.execute(crossterm::terminal::EnterAlternateScreen)?;
stdout.execute(Hide)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let mut events: Vec<String> = Vec::new();
let max_events = 100;
let mut event_stream = EventStream::new();
loop {
tokio::select! {
Some(Ok(crossterm_event)) = event_stream.next() => {
if let CrosstermEvent::Key(key) = crossterm_event {
match key.code {
KeyCode::Char('q') | KeyCode::Esc => break,
KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => break,
_ => {}
}
}
}
result = client.recv_event() => {
match result {
Ok(Some(event)) => {
let event_str = format_cuenv_event(&event);
events.push(event_str);
if events.len() > max_events {
events.remove(0);
}
}
Ok(None) => {
}
Err(e) => {
events.push(format!("[ERROR] Connection error: {e}"));
}
}
}
() = tokio::time::sleep(Duration::from_millis(100)) => {}
}
terminal.draw(|f| {
let area = f.area();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Min(5), Constraint::Length(3), ])
.split(area);
let header = Paragraph::new("cuenv Event Viewer")
.style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)
.alignment(Alignment::Center)
.block(Block::default().borders(Borders::ALL));
f.render_widget(header, chunks[0]);
let items: Vec<ListItem> = events
.iter()
.rev()
.take(chunks[1].height as usize - 2)
.rev()
.map(|e| ListItem::new(e.as_str()))
.collect();
let events_list = List::new(items)
.block(Block::default().borders(Borders::ALL).title(" Events "))
.style(Style::default().fg(Color::White));
f.render_widget(events_list, chunks[1]);
let footer = Paragraph::new("Press 'q' or Esc to quit | Ctrl+C to force exit")
.style(Style::default().fg(Color::DarkGray))
.alignment(Alignment::Center)
.block(Block::default().borders(Borders::ALL));
f.render_widget(footer, chunks[2]);
})?;
}
Ok(())
}
fn format_cuenv_event(event: &CuenvEvent) -> String {
use cuenv_events::EventCategory;
let timestamp = event.timestamp.format("%H:%M:%S%.3f");
let source = &event.source.target;
match &event.category {
EventCategory::Task(task_event) => {
use cuenv_events::TaskEvent;
match task_event {
TaskEvent::Started {
name,
command,
hermetic,
} => {
format!(
"[{timestamp}] {source} TASK {name} started: {command} (hermetic={hermetic})"
)
}
TaskEvent::CacheHit { name, cache_key } => {
format!("[{timestamp}] {source} CACHE HIT {name} key={cache_key}")
}
TaskEvent::Output {
name,
stream,
content,
} => {
format!("[{timestamp}] {source} OUTPUT {name}:{stream:?} {content}")
}
TaskEvent::Completed {
name,
success,
duration_ms,
..
} => {
let status = if *success { "✓" } else { "✗" };
format!(
"[{timestamp}] {source} TASK {name} completed {status} ({duration_ms}ms)"
)
}
_ => format!("[{timestamp}] {source} TASK {task_event:?}"),
}
}
EventCategory::Command(cmd_event) => {
format!("[{timestamp}] {source} CMD {cmd_event:?}")
}
EventCategory::System(sys_event) => {
format!("[{timestamp}] {source} SYS {sys_event:?}")
}
EventCategory::Output(out_event) => {
format!("[{timestamp}] {source} OUT {out_event:?}")
}
_ => format!("[{}] {} {:?}", timestamp, source, event.category),
}
}