use crate::executor::{ExecutionStatus, MultiNodeStreamManager};
use crate::ui::tui::progress::{extract_status_message, parse_progress_from_output};
use ratatui::{
layout::{Constraint, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph, Wrap},
Frame,
};
pub fn render(
f: &mut Frame,
manager: &MultiNodeStreamManager,
cluster_name: &str,
command: &str,
all_tasks_completed: bool,
) {
render_in_area(
f,
f.area(),
manager,
cluster_name,
command,
all_tasks_completed,
);
}
pub fn render_in_area(
f: &mut Frame,
area: Rect,
manager: &MultiNodeStreamManager,
cluster_name: &str,
command: &str,
all_tasks_completed: bool,
) {
let chunks = Layout::default()
.direction(ratatui::layout::Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Min(0), Constraint::Length(3), ])
.split(area);
render_header(f, chunks[0], cluster_name, command, manager);
render_node_list(f, chunks[1], manager);
render_footer(f, chunks[2], all_tasks_completed);
}
fn render_header(
f: &mut Frame,
area: Rect,
cluster_name: &str,
command: &str,
manager: &MultiNodeStreamManager,
) {
let total = manager.total_count();
let completed = manager.completed_count();
let failed = manager.failed_count();
let title = format!(" Cluster: {cluster_name} - {command} ");
let in_progress = total - completed - failed;
let status = format!(
" Total: {} • ✓ {} • ✗ {} • {} in progress ",
total, completed, failed, in_progress
);
let header_text = vec![Line::from(vec![
Span::styled(
title,
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::styled(status, Style::default().fg(Color::White)),
])];
let header = Paragraph::new(header_text).block(Block::default().borders(Borders::ALL));
f.render_widget(header, area);
}
fn render_node_list(f: &mut Frame, area: Rect, manager: &MultiNodeStreamManager) {
let streams = manager.streams();
let mut lines = Vec::new();
for (i, stream) in streams.iter().enumerate() {
let node_label = format!("[{}]", i + 1);
let node_name = &stream.node.host;
let (icon, color) = match stream.status() {
ExecutionStatus::Pending => ("⊙", Color::Gray),
ExecutionStatus::Running => ("⟳", Color::Blue),
ExecutionStatus::Completed => ("✓", Color::Green),
ExecutionStatus::Failed(msg) => {
lines.push(Line::from(vec![
Span::styled(format!("{node_label} "), Style::default().fg(Color::Yellow)),
Span::styled(
format!("{node_name:<20} "),
Style::default().fg(Color::White),
),
Span::styled("✗ ", Style::default().fg(Color::Red)),
Span::styled(msg, Style::default().fg(Color::Red)),
]));
continue;
}
};
let progress = parse_progress_from_output(stream.stdout());
let mut line_spans = vec![
Span::styled(format!("{node_label} "), Style::default().fg(Color::Yellow)),
Span::styled(
format!("{node_name:<20} "),
Style::default().fg(Color::White),
),
Span::styled(format!("{icon} "), Style::default().fg(color)),
];
if let Some(prog) = progress {
let bar_width = 20;
let filled = ((prog / 100.0) * bar_width as f32) as usize;
let bar = format!(
"[{}{}] {:>3.0}%",
"=".repeat(filled),
" ".repeat(bar_width - filled),
prog
);
line_spans.push(Span::styled(bar, Style::default().fg(Color::Cyan)));
if let Some(status_msg) = extract_status_message(stream.stdout()) {
let truncated = if status_msg.len() > 40 {
format!("{}...", &status_msg[..37])
} else {
status_msg
};
line_spans.push(Span::raw(" "));
line_spans.push(Span::styled(truncated, Style::default().fg(Color::Gray)));
}
} else {
let status_text = match stream.status() {
ExecutionStatus::Pending => "Waiting...".to_string(),
ExecutionStatus::Running => extract_status_message(stream.stdout())
.unwrap_or_else(|| "Running...".to_string()),
ExecutionStatus::Completed => {
if let Some(exit_code) = stream.exit_code() {
format!("Completed (exit: {exit_code})")
} else {
"Completed".to_string()
}
}
ExecutionStatus::Failed(_) => unreachable!(),
};
let truncated = if status_text.len() > 60 {
format!("{}...", &status_text[..57])
} else {
status_text
};
line_spans.push(Span::styled(truncated, Style::default().fg(Color::Gray)));
}
lines.push(Line::from(line_spans));
}
let paragraph = Paragraph::new(lines)
.block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
.wrap(Wrap { trim: false });
f.render_widget(paragraph, area);
}
fn render_footer(f: &mut Frame, area: Rect, all_tasks_completed: bool) {
let mut spans = vec![
Span::styled(" [1-9] ", Style::default().fg(Color::Yellow)),
Span::raw("Detail "),
Span::styled(" [s] ", Style::default().fg(Color::Yellow)),
Span::raw("Split "),
Span::styled(" [d] ", Style::default().fg(Color::Yellow)),
Span::raw("Diff "),
Span::styled(" [l] ", Style::default().fg(Color::Yellow)),
Span::raw("Log "),
Span::styled(" [q] ", Style::default().fg(Color::Yellow)),
Span::raw("Quit "),
Span::styled(" [?] ", Style::default().fg(Color::Yellow)),
Span::raw("Help "),
];
if all_tasks_completed {
spans.push(Span::raw(" │ "));
spans.push(Span::styled(
"✓ All tasks completed - Press 'q' or 'Esc' to exit",
Style::default()
.fg(Color::Green)
.add_modifier(Modifier::BOLD),
));
}
let help_text = Line::from(spans);
let footer = Paragraph::new(help_text).block(Block::default().borders(Borders::ALL));
f.render_widget(footer, area);
}