use crate::protocol::ProcessState;
use crate::tui::app::{App, InputMode, LineSource, StreamMode};
use ansi_to_tui::IntoText;
use ratatui::prelude::*;
use ratatui::widgets::*;
pub fn draw(frame: &mut Frame, app: &mut App) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Min(3), Constraint::Length(2), ])
.split(frame.area());
let main_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Length(22), Constraint::Min(30), ])
.split(chunks[0]);
draw_process_list(frame, app, main_chunks[0]);
draw_output(frame, app, main_chunks[1]);
draw_status_bar(frame, app, chunks[1]);
}
fn draw_process_list(frame: &mut Frame, app: &App, area: Rect) {
let items: Vec<ListItem> = app
.processes
.iter()
.enumerate()
.map(|(i, p)| {
let (indicator, style) = match p.state {
ProcessState::Running => ("●", Style::default().fg(Color::Green)),
ProcessState::Exited => {
if p.exit_code == Some(0) {
("✓", Style::default().fg(Color::DarkGray))
} else {
let code = p.exit_code.map(|c| format!(" ({})", c)).unwrap_or_default();
return ListItem::new(format!("✗ {}{}", p.name, code)).style(
if i == app.selected {
Style::default().fg(Color::Red).bg(Color::DarkGray)
} else {
Style::default().fg(Color::Red)
},
);
}
}
ProcessState::Failed => ("⟳", Style::default().fg(Color::Yellow)),
ProcessState::Unknown => ("?", Style::default().fg(Color::DarkGray)),
};
let mut label = p.name.clone();
if let Some(count) = p.restart_count
&& count > 0
{
if let Some(max) = p.max_restarts {
label = format!("{} ({}/{}↻)", label, count, max);
} else {
label = format!("{} ({}↻)", label, count);
}
}
if p.watched == Some(true) {
label = format!("{} W", label);
}
let text = format!("{} {}", indicator, label);
let style = if i == app.selected {
style.bg(Color::DarkGray)
} else {
style
};
ListItem::new(text).style(style)
})
.collect();
let list = List::new(items).block(Block::default().borders(Borders::ALL).title(" Processes "));
frame.render_widget(list, area);
}
fn ansi_line(text: &str, fallback_style: Option<Style>) -> Line<'static> {
match text.as_bytes().into_text() {
Ok(parsed) => {
if let Some(line) = parsed.lines.into_iter().next() {
line
} else {
Line::from(text.to_string())
}
}
Err(_) => {
let line = Line::from(text.to_string());
if let Some(style) = fallback_style {
line.style(style)
} else {
line
}
}
}
}
fn draw_output(frame: &mut Frame, app: &mut App, area: Rect) {
let name = app.selected_name().unwrap_or("(none)").to_string();
let mode_label = match app.stream_mode {
StreamMode::Stdout => "stdout",
StreamMode::Stderr => "stderr",
StreamMode::Both => "all",
};
let pause_indicator = if app.paused { " [PAUSED]" } else { "" };
let filter_indicator = match (&app.input_mode, &app.filter) {
(InputMode::FilterInput, _) => format!(" /{}", app.filter_buf),
(_, Some(pat)) => format!(" [filter: {}]", pat),
_ => String::new(),
};
let title = format!(
" Output: {} ({}){}{} ",
name, mode_label, pause_indicator, filter_indicator
);
let visible_height = area.height.saturating_sub(2) as usize; app.visible_height = visible_height;
let stderr_style = Style::default().fg(Color::Yellow);
let window = app.visible_lines(&name, visible_height).unwrap_or_default();
let lines: Vec<Line> = if window.is_empty() && !app.buffers.contains_key(&name) {
vec![Line::from("No output yet".to_string()).style(Style::default().fg(Color::DarkGray))]
} else {
window
.iter()
.map(|(src, l)| {
let style = if *src == LineSource::Stderr {
Some(stderr_style)
} else {
None
};
ansi_line(l, style)
})
.collect()
};
let paragraph =
Paragraph::new(lines).block(Block::default().borders(Borders::ALL).title(title));
frame.render_widget(paragraph, area);
}
fn draw_status_bar(frame: &mut Frame, app: &App, area: Rect) {
let keys = if app.input_mode == InputMode::FilterInput {
" type to filter, Enter confirm, Esc cancel ".to_string()
} else if app.paused {
" PgUp/u scroll up PgDn/d scroll down g top G bottom Space unpause / filter Esc clear filter ".to_string()
} else {
" ↑↓ select r restart x stop X stop-all e stream Space pause u/d scroll / filter q quit Q down ".to_string()
};
let failed = app.failed_count();
let counts = if failed > 0 {
format!(
" {} running, {} failed, {} exited ",
app.running_count(),
failed,
app.exited_count()
)
} else {
format!(
" {} running, {} exited ",
app.running_count(),
app.exited_count()
)
};
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Min(20),
Constraint::Length(counts.len() as u16 + 2),
])
.split(area);
let keys_widget = Paragraph::new(keys)
.style(Style::default().fg(Color::DarkGray))
.block(Block::default().borders(Borders::TOP));
let counts_widget = Paragraph::new(counts)
.style(Style::default().fg(Color::Cyan))
.alignment(Alignment::Right)
.block(Block::default().borders(Borders::TOP));
frame.render_widget(keys_widget, chunks[0]);
frame.render_widget(counts_widget, chunks[1]);
}