use super::text::{MIN_TITLE_WIDTH, display_width, truncate};
use crate::cli::dependency_display::{
DEP_ID_LIMIT, DepSummary, DependencyIndex, format_ids as format_dep_ids,
};
use pinto::backlog::{BacklogItem, ItemId};
use std::collections::{HashMap, HashSet};
pub(super) fn render_column_tree(
items: &[BacklogItem],
id_width: usize,
max_width: usize,
deps: &DependencyIndex,
) -> String {
let in_column: HashSet<&ItemId> = items.iter().map(|it| &it.id).collect();
let mut children: HashMap<ItemId, Vec<&BacklogItem>> = HashMap::new();
let mut roots: Vec<&BacklogItem> = Vec::new();
for it in items {
match tree_parent(it, &in_column) {
Some(parent) => children.entry(parent).or_default().push(it),
None => roots.push(it),
}
}
let mut renderer = TreeRenderer {
children,
visited: HashSet::new(),
id_width,
max_width,
deps,
out: String::new(),
};
for root in &roots {
renderer.render(root, "", true, true);
}
for it in items {
if !renderer.visited.contains(&it.id) {
renderer.render(it, "", true, true);
}
}
renderer.out
}
fn tree_parent(item: &BacklogItem, in_column: &HashSet<&ItemId>) -> Option<ItemId> {
let parent = item.parent.as_ref()?;
in_column.contains(parent).then(|| parent.clone())
}
struct TreeRenderer<'a> {
children: HashMap<ItemId, Vec<&'a BacklogItem>>,
visited: HashSet<&'a ItemId>,
id_width: usize,
max_width: usize,
deps: &'a DependencyIndex,
out: String,
}
impl<'a> TreeRenderer<'a> {
fn render(
&mut self,
node: &'a BacklogItem,
ancestor_prefix: &str,
is_last: bool,
is_root: bool,
) {
if !self.visited.insert(&node.id) {
return; }
let connector = if is_root {
""
} else if is_last {
"└─ "
} else {
"├─ "
};
let id_width = self.id_width;
let head = format!(
" {ancestor_prefix}{connector}{:<id_width$} ",
node.id.to_string()
);
let avail = self
.max_width
.saturating_sub(display_width(&head))
.max(MIN_TITLE_WIDTH);
self.out
.push_str(&format!("{head}{}\n", truncate(&node.title, avail)));
let child_prefix = if is_root {
String::new()
} else {
format!("{ancestor_prefix}{}", if is_last { " " } else { "│ " })
};
if let Some(marker) = dependency_marker_line(
&child_prefix,
id_width,
self.max_width,
&self.deps.summary(node),
) {
self.out.push_str(&marker);
self.out.push('\n');
}
let Some(kids) = self.children.get(&node.id) else {
return;
};
let kids: Vec<&BacklogItem> = kids.clone();
let last = kids.len() - 1;
for (i, kid) in kids.into_iter().enumerate() {
self.render(kid, &child_prefix, i == last, false);
}
}
}
pub(super) fn dependency_marker_line(
prefix: &str,
id_width: usize,
max_width: usize,
deps: &DepSummary,
) -> Option<String> {
if deps.is_empty() {
return None;
}
let indent = format!(" {prefix}{:id_width$} ", "");
let mut body = String::new();
if !deps.depends_on.is_empty() {
let mark = if deps.blocked { "⊸!" } else { "⊸" };
body.push_str(&format!(
"{mark} {}",
format_dep_ids(&deps.depends_on, DEP_ID_LIMIT)
));
}
if !deps.dependents.is_empty() {
if !body.is_empty() {
body.push_str(" ");
}
body.push_str(&format!(
"⊷ {}",
format_dep_ids(&deps.dependents, DEP_ID_LIMIT)
));
}
let avail = max_width
.saturating_sub(display_width(&indent))
.max(MIN_TITLE_WIDTH);
Some(format!("{indent}{}", truncate(&body, avail)))
}