use crate::model::Issue;
use crate::output::Theme;
use rich_rust::prelude::*;
use unicode_width::UnicodeWidthChar;
use unicode_width::UnicodeWidthStr;
pub struct DependencyTree<'a> {
root_issue: &'a Issue,
all_issues: &'a [Issue],
theme: &'a Theme,
max_depth: usize,
}
impl<'a> DependencyTree<'a> {
#[must_use]
pub fn new(root: &'a Issue, all: &'a [Issue], theme: &'a Theme) -> Self {
Self {
root_issue: root,
all_issues: all,
theme,
max_depth: 10,
}
}
#[must_use]
pub fn max_depth(mut self, depth: usize) -> Self {
self.max_depth = depth;
self
}
#[must_use]
pub fn build(&self) -> Tree {
let root_node = self.build_node(self.root_issue, 0);
Tree::new(root_node)
.guides(TreeGuides::Rounded)
.guide_style(self.theme.dimmed.clone())
}
fn build_node(&self, issue: &Issue, depth: usize) -> TreeNode {
let label = format!(
"{} [{}] {}",
issue.id,
format!("{}", issue.status).chars().next().unwrap_or('?'),
truncate(&issue.title, 40)
);
let mut node = TreeNode::new(Text::new(label));
if depth < self.max_depth {
for dep in &issue.dependencies {
if let Some(dep_issue) = self.find_issue(&dep.depends_on_id) {
let child = self.build_node(dep_issue, depth + 1);
node = node.child(child);
} else {
let missing =
TreeNode::new(Text::new(format!("{} [?] (not found)", dep.depends_on_id)));
node = node.child(missing);
}
}
}
node
}
fn find_issue(&self, id: &str) -> Option<&Issue> {
self.all_issues.iter().find(|i| i.id == id)
}
}
fn truncate(s: &str, max: usize) -> String {
let width = UnicodeWidthStr::width(s);
if width <= max {
s.to_string()
} else if max <= 3 {
let mut w = 0;
let mut out = String::new();
for c in s.chars() {
let cw = UnicodeWidthChar::width(c).unwrap_or(0);
if w + cw > max {
break;
}
w += cw;
out.push(c);
}
out
} else {
let target = max - 3;
let mut w = 0;
let mut out = String::new();
for c in s.chars() {
let cw = UnicodeWidthChar::width(c).unwrap_or(0);
if w + cw > target {
break;
}
w += cw;
out.push(c);
}
out.push_str("...");
out
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dep_tree_truncation_safe() {
let title = "😊".repeat(20); let truncated = truncate(&title, 10);
assert!(UnicodeWidthStr::width(truncated.as_str()) <= 10);
assert!(truncated.starts_with("😊"));
assert!(truncated.ends_with("..."));
}
#[test]
fn test_dep_tree_truncation_ascii() {
let title = "Hello, World! This is a long title";
let truncated = truncate(title, 15);
assert!(UnicodeWidthStr::width(truncated.as_str()) <= 15);
assert!(truncated.ends_with("..."));
}
#[test]
fn test_dep_tree_truncation_short() {
let title = "Short";
let truncated = truncate(title, 40);
assert_eq!(truncated, "Short");
}
}