use ratatui::Frame;
use ratatui::layout::Rect;
use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use ratatui::widgets::Paragraph;
use tmprl_core::WorkflowStatus;
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;
const GUTTER: usize = 5;
const STATUS: usize = 15;
const TYPE: usize = 22;
const AGE: usize = 5;
const NAMESPACE: usize = 16;
pub fn render(frame: &mut Frame, area: Rect, view: &View, app: &App, t: &Theme) {
if area.height == 0 {
return;
}
let rows = view.workflow_rows();
if rows.is_empty() {
let msg = if view.workflows.is_loading() {
"loading workflows…".to_string()
} else if let Some(e) = view.workflows.error() {
format!("{e} (R to retry)")
} else if view.query.trim().is_empty() {
"no workflows in this namespace".to_string()
} else {
"no workflows match this query (i to edit it)".to_string()
};
frame.render_widget(
Paragraph::new(Span::styled(format!(" {msg}"), Style::new().fg(t.faint))),
area,
);
return;
}
let show_ns = view.is_fanned_out();
let fixed = GUTTER + STATUS + TYPE + AGE + if show_ns { NAMESPACE } else { 0 };
let id_width = (area.width as usize).saturating_sub(fixed).max(8);
let now = now_millis();
let height = area.height as usize;
let first = app
.view
.cursor
.saturating_sub(height.saturating_sub(1) / 2)
.min(rows.len().saturating_sub(height));
let lines: Vec<Line> = rows
.iter()
.enumerate()
.skip(first)
.take(height)
.map(|(i, w)| {
let focused = i == view.cursor;
let base = if focused {
Style::new().fg(t.fg).bg(t.sel).add_modifier(Modifier::BOLD)
} else if view.is_selected(i) {
Style::new().fg(t.fg).bg(t.sel)
} else {
Style::new().fg(t.fg)
};
let mut spans = vec![
Span::styled(
super::gutter(i, view.cursor),
Style::new().fg(if focused { t.warn } else { t.faint }),
),
Span::styled(
format!(
"{} {:<width$}",
w.status.glyph(),
truncate(w.status.query_name(), STATUS - 2),
width = STATUS - 2
),
Style::new().fg(status_color(w.status, t)),
),
Span::styled(
format!("{:<id_width$}", truncate(&w.workflow_id, id_width)),
base,
),
Span::styled(
format!(
"{:<width$}",
truncate(&w.workflow_type, TYPE - 1),
width = TYPE
),
Style::new().fg(t.dim),
),
];
if show_ns {
spans.push(Span::styled(
format!(
"{:<width$}",
truncate(&w.namespace, NAMESPACE - 1),
width = NAMESPACE
),
Style::new().fg(t.accent),
));
}
spans.push(Span::styled(
match w.start_time {
Some(started) => {
format!("{:>width$}", humanize_age_ms(now - started), width = AGE)
}
None => format!("{:>AGE$}", ","),
},
Style::new().fg(t.faint),
));
Line::from(spans)
})
.map(|l| highlight(l, &app.search, match_style()))
.collect();
frame.render_widget(Paragraph::new(lines), area);
}
fn status_color(s: WorkflowStatus, t: &Theme) -> ratatui::style::Color {
match s {
WorkflowStatus::Running => t.accent,
WorkflowStatus::Completed => t.ok,
WorkflowStatus::Failed | WorkflowStatus::TimedOut => t.err,
WorkflowStatus::Terminated | WorkflowStatus::Canceled => t.warn,
WorkflowStatus::ContinuedAsNew | WorkflowStatus::Paused => t.dim,
WorkflowStatus::Unspecified => t.faint,
}
}
fn now_millis() -> i64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0)
}