use std::collections::BTreeSet;
use rhei_core::ast::Task;
use crate::rhei_output::common::{fmt_prior_list, rhei_groups, title_case_kind, RheiGroup};
pub struct ProgressReportOutput {
pub color: bool,
pub show_dependencies: bool,
pub terminal_ids: BTreeSet<String>,
pub is_project: bool,
}
impl ProgressReportOutput {
pub fn plain(color: bool, show_dependencies: bool) -> Self {
Self { color, show_dependencies, terminal_ids: BTreeSet::new(), is_project: false }
}
pub fn to_string(&self, rhei: &rhei_core::ast::Rhei) -> String {
let mut out = String::new();
out.push_str(if self.is_project { "Panta: " } else { "Rhei: " });
out.push_str(&rhei.title);
out.push('\n');
if let Some(summary) = self.summary_line(rhei) {
out.push_str(&summary);
out.push('\n');
}
for section in &rhei.content_sections {
if section.rhei.is_some() {
continue;
}
self.render_section(§ion.title, §ion.content, 0, &mut out);
}
let groups = rhei_groups(rhei);
if groups.is_empty() {
for task in &rhei.tasks {
self.render_node(task, 0, &mut out);
}
return out;
}
for group in &groups {
out.push('\n');
out.push_str(&group.heading());
out.push('\n');
for section in &rhei.content_sections {
if section.rhei.as_deref() != Some(group.id.as_str()) || section.content.is_empty()
{
continue;
}
self.render_section(
group.section_title(§ion.title),
§ion.content,
1,
&mut out,
);
}
self.render_group_tasks(rhei, group, &mut out);
}
out
}
fn render_group_tasks(&self, rhei: &rhei_core::ast::Rhei, group: &RheiGroup, out: &mut String) {
let mut empty = true;
for task in rhei.tasks.iter().filter(|task| group.owns(task)) {
self.render_node(task, 0, out);
empty = false;
}
if empty {
out.push_str(" (no tickets yet)\n");
}
}
fn render_section(&self, title: &str, content: &str, indent: usize, out: &mut String) {
let pad = " ".repeat(indent);
out.push_str(&pad);
out.push_str(title);
out.push_str(":\n");
for line in content.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
out.push_str(&pad);
out.push_str(" ");
out.push_str(trimmed);
out.push('\n');
}
}
}
fn summary_line(&self, rhei: &rhei_core::ast::Rhei) -> Option<String> {
if self.terminal_ids.is_empty() {
return None;
}
let (total, done) = count_tickets(&rhei.tasks, &self.terminal_ids);
if total == 0 {
return None;
}
let percent = done * 100 / total;
Some(format!("{done}/{total} tickets done ({percent}%)"))
}
fn render_node(&self, task: &Task, indent_level: usize, out: &mut String) {
let state_upper = task.state.trim().to_ascii_uppercase();
let badge = badge_for(&state_upper, self.color);
if indent_level == 0 {
out.push_str("* ");
} else {
for _ in 0..indent_level {
out.push_str(" ");
}
out.push_str("- ");
}
out.push_str(&title_case_kind(&task.kind));
out.push(' ');
out.push_str(&task.id.to_string());
out.push_str(": ");
out.push_str(&task.title);
out.push_str(" ");
out.push_str(&badge);
out.push('\n');
if self.show_dependencies && indent_level == 0 && !task.prior.is_empty() {
out.push_str(" - Prior: ");
out.push_str(&fmt_prior_list(&task.prior));
out.push('\n');
}
for child in &task.children {
self.render_node(child, indent_level + 1, out);
}
}
}
fn count_tickets(tasks: &[Task], terminal_ids: &BTreeSet<String>) -> (usize, usize) {
let mut total = 0;
let mut done = 0;
for task in tasks {
total += 1;
if terminal_ids.contains(&task.id.to_string()) {
done += 1;
}
let (child_total, child_done) = count_tickets(&task.children, terminal_ids);
total += child_total;
done += child_done;
}
(total, done)
}
fn badge_for(state_upper: &str, color: bool) -> String {
if !color {
return format!("[{}]", state_upper);
}
let key = state_upper.to_ascii_lowercase().replace(' ', "-");
let code = match key.as_str() {
"pending" => 34, "in-progress" => 33, "blocked" => 31, "completed" => 32, "cancelled" => 90, _ => 35, };
format!("\x1b[{}m[{}]\x1b[0m", code, state_upper)
}
pub fn to_progress_report(rhei: &rhei_core::ast::Rhei) -> String {
ProgressReportOutput::plain(true, true).to_string(rhei)
}