use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use tmprl_core::history::{Category, Outcome};
use tmprl_core::outline::{Outline, Row};
use tmprl_core::workflow::humanize_age_ms;
use super::truncate;
use super::{highlight, match_style};
use crate::app::App;
use crate::theme::Theme;
use crate::view::View;
pub fn render(frame: &mut Frame, area: Rect, view: &View, app: &App, t: &Theme) {
if area.height == 0 {
return;
}
let Some(outline) = view.history.value() else {
let msg = match view.history.error() {
Some(e) => format!("{e} (R to retry)"),
None => "loading history…".to_string(),
};
frame.render_widget(
Paragraph::new(Span::styled(format!(" {msg}"), Style::new().fg(t.faint))),
area,
);
return;
};
if outline.is_empty() {
let msg = if outline.show_plumbing() {
"no events"
} else {
"nothing but workflow tasks (zp to show them)"
};
frame.render_widget(
Paragraph::new(Span::styled(format!(" {msg}"), Style::new().fg(t.faint))),
area,
);
return;
}
let height = area.height as usize;
let first = app
.view
.cursor
.saturating_sub(height.saturating_sub(1) / 2)
.min(outline.len().saturating_sub(height));
let lines: Vec<Line> = outline
.slice(first, height)
.into_iter()
.enumerate()
.map(|(n, row)| render_row(outline, row, first + n, view, t))
.map(|l| highlight(l, &app.search, match_style()))
.collect();
frame.render_widget(Paragraph::new(lines), area);
}
fn render_row<'a>(
outline: &'a Outline,
row: Row,
index: usize,
view: &View,
t: &Theme,
) -> Line<'a> {
let focused = index == view.cursor;
let base = if focused {
Style::new().fg(t.fg).bg(t.sel).add_modifier(Modifier::BOLD)
} else if view.is_selected(index) {
Style::new().fg(t.fg).bg(t.sel)
} else {
Style::new().fg(t.fg)
};
let gutter = Span::styled(
super::gutter(index, view.cursor),
Style::new().fg(if focused { t.warn } else { t.faint }),
);
match row {
Row::Group { group, expanded } => {
let Some(g) = outline.group(group) else {
return Line::from(gutter);
};
let mut spans = vec![
gutter,
Span::styled(
if g.events.len() > 1 {
if expanded { "▾ " } else { "▸ " }
} else {
" "
},
Style::new().fg(t.faint),
),
Span::styled(
format!("{} ", outcome_glyph(g.outcome)),
Style::new().fg(outcome_color(g.outcome, t)),
),
Span::styled(
format!("{:<14}", category_label(g.category)),
Style::new().fg(t.dim),
),
Span::styled(format!("{:<32}", truncate(&g.subject, 31)), base),
];
if g.attempts > 1 {
spans.push(Span::styled(
format!("×{} ", g.attempts),
Style::new().fg(t.warn).add_modifier(Modifier::BOLD),
));
}
spans.push(Span::styled(
match g.duration_ms() {
Some(d) => format!("{:>6}", humanize_age_ms(d)),
None if g.is_open() => format!("{:>6}", "…"),
None => format!("{:>6}", ""),
},
Style::new().fg(t.faint),
));
if let Some(f) = &g.failure {
spans.push(Span::styled(
format!(" {}", truncate(f, 48)),
Style::new().fg(t.err),
));
}
Line::from(spans)
}
Row::Event { event, .. } => {
let Some(e) = outline.event(event) else {
return Line::from(gutter);
};
let mut spans = vec![
gutter,
Span::styled(format!(" {:>5} ", e.id), Style::new().fg(t.faint)),
Span::styled(format!("{:<38}", truncate(e.name, 37)), base),
];
let detail = e
.fields
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join(" ");
if !detail.is_empty() {
spans.push(Span::styled(truncate(&detail, 40), Style::new().fg(t.dim)));
}
if let Some(f) = &e.failure {
spans.push(Span::styled(
format!(" {}", truncate(f, 40)),
Style::new().fg(t.err),
));
}
Line::from(spans)
}
}
}
fn outcome_glyph(o: Outcome) -> char {
match o {
Outcome::Pending => '●',
Outcome::Completed => '✓',
Outcome::Failed => '✗',
Outcome::Canceled => '⊘',
Outcome::TimedOut => '◔',
Outcome::Terminated => '■',
Outcome::ContinuedAsNew => '↻',
Outcome::Rejected => '⊗',
}
}
fn outcome_color(o: Outcome, t: &Theme) -> ratatui::style::Color {
match o {
Outcome::Pending => t.accent,
Outcome::Completed => t.ok,
Outcome::Failed | Outcome::TimedOut | Outcome::Rejected => t.err,
Outcome::Terminated | Outcome::Canceled => t.warn,
Outcome::ContinuedAsNew => t.dim,
}
}
pub(crate) fn category_label(c: Category) -> &'static str {
match c {
Category::Workflow => "workflow",
Category::WorkflowTask => "task",
Category::Activity => "activity",
Category::Timer => "timer",
Category::ChildWorkflow => "child",
Category::ExternalWorkflow => "external",
Category::Update => "update",
Category::Nexus => "nexus",
Category::Marker => "marker",
Category::SearchAttributes => "search-attr",
}
}