1use std::path::Path;
2
3use anyhow::Result;
4
5use crate::index::Index;
6use crate::unit::Status;
7use crate::util::natural_cmp;
8
9pub fn cmd_tree(mana_dir: &Path, id: Option<&str>) -> Result<()> {
13 let index = Index::load_or_rebuild(mana_dir)?;
14
15 if let Some(unit_id) = id {
16 print_subtree(&index, unit_id)?;
18 } else {
19 print_full_tree(&index);
21 }
22
23 Ok(())
24}
25
26fn print_full_tree(index: &Index) {
27 let root_units: Vec<_> = index.units.iter().filter(|e| e.parent.is_none()).collect();
29
30 if root_units.is_empty() {
31 println!("No units found.");
32 return;
33 }
34
35 let mut visited = std::collections::HashSet::new();
36 for root in root_units {
37 print_tree_node(index, &root.id, "", &mut visited);
38 }
39}
40
41fn print_subtree(index: &Index, unit_id: &str) -> Result<()> {
42 let _entry = index
43 .units
44 .iter()
45 .find(|e| e.id == unit_id)
46 .ok_or_else(|| anyhow::anyhow!("Unit {} not found", unit_id))?;
47
48 let mut visited = std::collections::HashSet::new();
49 print_tree_node(index, unit_id, "", &mut visited);
50
51 Ok(())
52}
53
54fn print_tree_node(
55 index: &Index,
56 unit_id: &str,
57 prefix: &str,
58 visited: &mut std::collections::HashSet<String>,
59) {
60 if visited.contains(unit_id) {
61 return;
62 }
63 visited.insert(unit_id.to_string());
64
65 if let Some(entry) = index.units.iter().find(|e| e.id == unit_id) {
67 let status_indicator = match entry.status {
68 Status::Open => "[ ]",
69 Status::InProgress | Status::AwaitingVerify => "[-]",
70 Status::Closed => "[x]",
71 };
72
73 println!(
74 "{}{} {} {}",
75 prefix, status_indicator, entry.id, entry.title
76 );
77 } else {
78 println!("{}[!] {}", prefix, unit_id);
79 return;
80 }
81
82 let children: Vec<_> = index
84 .units
85 .iter()
86 .filter(|e| e.parent.as_ref() == Some(&unit_id.to_string()))
87 .collect();
88
89 let dependents: Vec<_> = index
91 .units
92 .iter()
93 .filter(|e| e.dependencies.contains(&unit_id.to_string()))
94 .collect();
95
96 let mut all_children = children;
98 for dep in dependents {
99 if !all_children.iter().any(|e| e.id == dep.id) {
100 all_children.push(dep);
101 }
102 }
103
104 all_children.sort_by(|a, b| natural_cmp(&a.id, &b.id));
106
107 for (i, child) in all_children.iter().enumerate() {
108 let is_last_child = i == all_children.len() - 1;
109 let connector = if is_last_child {
110 "└── "
111 } else {
112 "├── "
113 };
114 let new_prefix = if is_last_child {
115 format!("{} ", prefix)
116 } else {
117 format!("{}│ ", prefix)
118 };
119
120 print!("{}{}", prefix, connector);
121 print_tree_node(index, &child.id, &new_prefix, visited);
122 }
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128 use crate::unit::Unit;
129 use std::fs;
130 use tempfile::TempDir;
131
132 fn setup_test_units() -> (TempDir, std::path::PathBuf) {
133 let dir = TempDir::new().unwrap();
134 let mana_dir = dir.path().join(".mana");
135 fs::create_dir(&mana_dir).unwrap();
136
137 let unit1 = Unit::new("1", "Root task");
145 let mut unit1_1 = Unit::new("1.1", "Subtask");
146 unit1_1.parent = Some("1".to_string());
147 let mut unit1_2 = Unit::new("1.2", "Another subtask");
148 unit1_2.parent = Some("1".to_string());
149 let unit2 = Unit::new("2", "Another root");
150 let mut unit3 = Unit::new("3", "Depends on 1");
151 unit3.dependencies = vec!["1".to_string()];
152
153 unit1.to_file(mana_dir.join("1.yaml")).unwrap();
154 unit1_1.to_file(mana_dir.join("1.1.yaml")).unwrap();
155 unit1_2.to_file(mana_dir.join("1.2.yaml")).unwrap();
156 unit2.to_file(mana_dir.join("2.yaml")).unwrap();
157 unit3.to_file(mana_dir.join("3.yaml")).unwrap();
158
159 (dir, mana_dir)
160 }
161
162 #[test]
163 fn full_tree_displays() {
164 let (_dir, mana_dir) = setup_test_units();
165 let index = Index::load_or_rebuild(&mana_dir).unwrap();
166
167 print_full_tree(&index);
169 }
170
171 #[test]
172 fn subtree_works() {
173 let (_dir, mana_dir) = setup_test_units();
174 let index = Index::load_or_rebuild(&mana_dir).unwrap();
175
176 let _ = print_subtree(&index, "1");
178 }
179
180 #[test]
181 fn subtree_not_found() {
182 let (_dir, mana_dir) = setup_test_units();
183 let index = Index::load_or_rebuild(&mana_dir).unwrap();
184
185 let result = print_subtree(&index, "nonexistent");
186 assert!(result.is_err());
187 }
188
189 #[test]
190 fn status_indicators() {
191 let dir = TempDir::new().unwrap();
192 let mana_dir = dir.path().join(".mana");
193 fs::create_dir(&mana_dir).unwrap();
194
195 let b1 = Unit::new("1", "Open task");
196 let mut b2 = Unit::new("2", "In progress");
197 b2.status = crate::unit::Status::InProgress;
198 let mut b3 = Unit::new("3", "Closed");
199 b3.status = crate::unit::Status::Closed;
200
201 b1.to_file(mana_dir.join("1.yaml")).unwrap();
202 b2.to_file(mana_dir.join("2.yaml")).unwrap();
203 b3.to_file(mana_dir.join("3.yaml")).unwrap();
204
205 let index = Index::load_or_rebuild(&mana_dir).unwrap();
206 print_full_tree(&index);
207 }
208}