use crate::executor::{ExecutionStatus, NodeStream};
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,
stream: &NodeStream,
node_index: usize,
scroll_pos: usize,
follow_mode: bool,
all_tasks_completed: bool,
) {
let chunks = Layout::default()
.direction(ratatui::layout::Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Min(0), Constraint::Length(3), ])
.split(f.area());
render_header(f, chunks[0], stream, node_index);
render_output(f, chunks[1], stream, scroll_pos, follow_mode);
render_footer(f, chunks[2], follow_mode, all_tasks_completed);
}
fn render_header(f: &mut Frame, area: Rect, stream: &NodeStream, node_index: usize) {
let node = &stream.node;
let status_text = match stream.status() {
ExecutionStatus::Pending => ("Pending", Color::Gray),
ExecutionStatus::Running => ("Running", Color::Blue),
ExecutionStatus::Completed => ("Completed", Color::Green),
ExecutionStatus::Failed(msg) => {
let title = format!(
" [{}] {}:{} ({}) - Failed: {} ",
node_index + 1,
node.host,
node.port,
node.username,
msg
);
let header = Paragraph::new(Line::from(Span::styled(
title,
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
)))
.block(Block::default().borders(Borders::ALL));
f.render_widget(header, area);
return;
}
};
let title = format!(
" [{}] {}:{} ({}) - {} ",
node_index + 1,
node.host,
node.port,
node.username,
status_text.0
);
let header = Paragraph::new(Line::from(Span::styled(
title,
Style::default()
.fg(status_text.1)
.add_modifier(Modifier::BOLD),
)))
.block(Block::default().borders(Borders::ALL));
f.render_widget(header, area);
}
fn render_output(
f: &mut Frame,
area: Rect,
stream: &NodeStream,
scroll_pos: usize,
follow_mode: bool,
) {
let stdout = String::from_utf8_lossy(stream.stdout());
let stderr = String::from_utf8_lossy(stream.stderr());
let mut lines: Vec<Line> = Vec::new();
for line in stdout.lines() {
lines.push(Line::from(line.to_string()));
}
if !stderr.is_empty() {
lines.push(Line::from(Span::styled(
"--- stderr ---",
Style::default().fg(Color::Red).add_modifier(Modifier::BOLD),
)));
for line in stderr.lines() {
lines.push(Line::from(Span::styled(
line.to_string(),
Style::default().fg(Color::Red),
)));
}
}
if lines.is_empty() {
lines.push(Line::from(Span::styled(
"(no output yet)",
Style::default().fg(Color::Gray),
)));
}
let viewport_height = area.height.saturating_sub(2) as usize; let total_lines = lines.len();
let viewport_height = viewport_height.max(1);
let max_scroll = total_lines.saturating_sub(viewport_height);
let scroll = if follow_mode {
max_scroll
} else {
scroll_pos.min(max_scroll)
};
let scroll = scroll.min(total_lines.saturating_sub(1));
let paragraph = Paragraph::new(lines)
.block(Block::default().borders(Borders::LEFT | Borders::RIGHT))
.scroll((scroll as u16, 0))
.wrap(Wrap { trim: false });
f.render_widget(paragraph, area);
}
fn render_footer(f: &mut Frame, area: Rect, follow_mode: bool, all_tasks_completed: bool) {
let follow_indicator = if follow_mode {
Span::styled("[FOLLOW] ", Style::default().fg(Color::Green))
} else {
Span::raw("")
};
let mut spans = vec![
follow_indicator,
Span::styled(" [←/→] ", Style::default().fg(Color::Yellow)),
Span::raw("Switch "),
Span::styled(" [Esc] ", Style::default().fg(Color::Yellow)),
Span::raw("Summary "),
Span::styled(" [↑/↓] ", Style::default().fg(Color::Yellow)),
Span::raw("Scroll "),
Span::styled(" [f] ", Style::default().fg(Color::Yellow)),
Span::raw("Follow "),
Span::styled(" [q] ", Style::default().fg(Color::Yellow)),
Span::raw("Quit "),
];
if all_tasks_completed {
spans.push(Span::raw(" │ "));
spans.push(Span::styled(
"✓ All tasks completed",
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);
}