use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::{Block, Borders, Paragraph};
use tmprl_core::history::NormalizedEvent;
use tmprl_core::outline::{Outline, Row};
use tmprl_core::payload::Rendered;
use crate::app::{App, DecodeState};
use crate::theme::Theme;
use crate::view::View;
pub fn render(frame: &mut Frame, area: Rect, view: &View, app: &App, t: &Theme) -> usize {
if area.height < 2 {
return 0;
}
if let Some(piped) = view.piped.clone() {
return render_piped(frame, area, view, &piped, t);
}
let Some(outline) = view.history.value() else {
return 0;
};
let lines = match outline.row_at(view.cursor) {
Some(Row::Event { event, .. }) => outline
.event(event)
.map(|e| event_lines(e, app, t))
.unwrap_or_default(),
Some(Row::Group { group, .. }) => group_lines(outline, group, app, t),
None => Vec::new(),
};
let lines = if lines.is_empty() {
vec![Line::from(Span::styled(
" nothing carried here",
Style::new().fg(t.faint),
))]
} else {
lines
};
let visible = area.height.saturating_sub(1) as usize;
let max_scroll = lines.len().saturating_sub(visible);
let scroll = view.detail_scroll.min(max_scroll);
let title = if max_scroll == 0 {
" payloads (K to close) ".to_string()
} else {
format!(
" payloads (<C-e>/<C-y> to scroll, {}/{}, K to close) ",
scroll + 1,
max_scroll + 1
)
};
let block = Block::default()
.borders(Borders::TOP)
.border_style(Style::new().fg(t.faint))
.title(Span::styled(title, Style::new().fg(t.accent)));
let inner = block.inner(area);
frame.render_widget(block, area);
frame.render_widget(Paragraph::new(lines).scroll((scroll as u16, 0)), inner);
max_scroll
}
fn group_lines<'a>(outline: &'a Outline, group: usize, app: &App, t: &Theme) -> Vec<Line<'a>> {
let Some(g) = outline.group(group) else {
return Vec::new();
};
let mut lines = vec![Line::from(vec![
Span::styled(
format!(" {} ", g.subject),
Style::new().fg(t.fg).add_modifier(Modifier::BOLD),
),
Span::styled(
format!("{} {} event(s)", g.outcome.label(), g.events.len()),
Style::new().fg(t.dim),
),
])];
if let Some(f) = &g.failure {
lines.push(Line::from(Span::styled(
format!(" {f}"),
Style::new().fg(t.err),
)));
}
let before = lines.len();
for id in g.payload_ends() {
let Some(e) = outline.events().iter().find(|e| e.id == id) else {
continue;
};
if e.payloads.is_empty() {
continue;
}
lines.extend(payload_lines(e, app, t));
}
if lines.len() == before {
lines.push(Line::from(Span::styled(
" no payloads on this group",
Style::new().fg(t.faint),
)));
}
lines
}
fn event_lines<'a>(e: &'a NormalizedEvent, app: &App, t: &Theme) -> Vec<Line<'a>> {
let mut lines = vec![Line::from(vec![
Span::styled(
format!(" {} ", e.name),
Style::new().fg(t.fg).add_modifier(Modifier::BOLD),
),
Span::styled(format!("event {}", e.id), Style::new().fg(t.dim)),
])];
for (k, v) in &e.fields {
lines.push(Line::from(vec![
Span::styled(format!(" {k} = "), Style::new().fg(t.faint)),
Span::styled(v.clone(), Style::new().fg(t.fg)),
]));
}
if let Some(f) = &e.failure {
lines.push(Line::from(Span::styled(
format!(" {f}"),
Style::new().fg(t.err),
)));
}
lines.extend(payload_lines(e, app, t));
lines
}
fn payload_lines<'a>(e: &'a NormalizedEvent, app: &App, t: &Theme) -> Vec<Line<'a>> {
let mut lines = Vec::new();
for (label, p) in &e.payloads {
lines.push(Line::from(Span::styled(
format!(" {label}"),
Style::new().fg(t.accent).add_modifier(Modifier::BOLD),
)));
match p.render() {
Rendered::Text(text) => {
for l in text.lines() {
lines.push(Line::from(Span::styled(
format!(" {l}"),
Style::new().fg(t.fg),
)));
}
}
Rendered::Null => lines.push(Line::from(Span::styled(
" null",
Style::new().fg(t.faint),
))),
Rendered::Opaque { bytes, encoding } => lines.push(Line::from(Span::styled(
format!(" {encoding}, {bytes} bytes, not shown"),
Style::new().fg(t.faint),
))),
Rendered::Encrypted { bytes, encoding } => {
let (what, style) = match app.decode_state(p) {
DecodeState::NoCodec => (
format!(
" ๐ {encoding}, {bytes} bytes, set a codec endpoint in config.toml"
),
Style::new().fg(t.warn),
),
DecodeState::InFlight => (
format!(" ๐ {encoding}, {bytes} bytes, decodingโฆ"),
Style::new().fg(t.dim),
),
DecodeState::Idle => (
format!(" ๐ {encoding}, {bytes} bytes"),
Style::new().fg(t.warn),
),
DecodeState::Failed(why) => (
format!(" ๐ {encoding}, {bytes} bytes, codec: {why} (R to retry)"),
Style::new().fg(t.err),
),
};
lines.push(Line::from(Span::styled(what, style)));
}
}
}
lines
}
fn render_piped(
frame: &mut Frame,
area: Rect,
view: &View,
piped: &Result<String, String>,
t: &Theme,
) -> usize {
let (body, style, label) = match piped {
Ok(out) => (out, Style::new().fg(t.fg), "filtered"),
Err(err) => (err, Style::new().fg(t.err), "filter failed"),
};
let lines: Vec<Line> = if body.trim().is_empty() {
vec![Line::from(Span::styled(
" (no output)",
Style::new().fg(t.faint),
))]
} else {
body.lines()
.map(|l| Line::from(Span::styled(format!(" {l}"), style)))
.collect()
};
let visible = area.height.saturating_sub(1) as usize;
let max_scroll = lines.len().saturating_sub(visible);
let scroll = view.detail_scroll.min(max_scroll);
let title = if max_scroll == 0 {
format!(" {label} (K to close) ")
} else {
format!(
" {label} (<C-e>/<C-y> to scroll, {}/{}, K to close) ",
scroll + 1,
max_scroll + 1
)
};
let block = Block::default()
.borders(Borders::TOP)
.border_style(Style::new().fg(if piped.is_err() { t.err } else { t.faint }))
.title(Span::styled(title, Style::new().fg(t.accent)));
let inner = block.inner(area);
frame.render_widget(block, area);
frame.render_widget(Paragraph::new(lines).scroll((scroll as u16, 0)), inner);
max_scroll
}