1use anyhow::Result;
2use colored::Colorize;
3use std::path::PathBuf;
4
5use crate::commands::helpers::resolve_group_tag;
6use crate::formats::{natural_sort_ids, serialize_scg};
7use crate::models::{Phase, Priority, TaskStatus};
8use crate::storage::Storage;
9
10fn format_status(status: &TaskStatus) -> String {
12 match status {
13 TaskStatus::Pending => "○ Pending".normal().to_string(),
14 TaskStatus::InProgress => "◐ In Progress".yellow().to_string(),
15 TaskStatus::Done => "● Done".green().to_string(),
16 TaskStatus::Review => "◑ Review".cyan().to_string(),
17 TaskStatus::Blocked => "✗ Blocked".red().to_string(),
18 TaskStatus::Deferred => "◌ Deferred".dimmed().to_string(),
19 TaskStatus::Cancelled => "⊘ Cancelled".dimmed().to_string(),
20 TaskStatus::Expanded => "◈ Expanded".blue().to_string(),
21 TaskStatus::Failed => "✗ Failed".red().bold().to_string(),
22 }
23}
24
25fn format_priority(priority: &Priority) -> String {
27 match priority {
28 Priority::Critical => "Crit".red().bold().to_string(),
29 Priority::High => "High".yellow().to_string(),
30 Priority::Medium => "Med".normal().to_string(),
31 Priority::Low => "Low".dimmed().to_string(),
32 }
33}
34
35fn format_agent_type(agent_type: &Option<String>) -> String {
37 match agent_type {
38 Some(at) => at.clone(),
39 None => "-".to_string(),
40 }
41}
42
43fn format_task_id(id: &str) -> String {
46 if id.len() > 12 {
47 format!("{}...", &id[..8])
48 } else {
49 id.to_string()
50 }
51}
52
53fn print_human_readable(phase: &Phase, phase_tag: &str) {
55 println!("{} {}\n", "Phase:".blue().bold(), phase_tag.cyan());
56
57 if phase.tasks.is_empty() {
58 println!("{}", "(no tasks)".dimmed());
59 return;
60 }
61
62 println!(
64 "{:>4} {:<11} {:<32} {:<14} {:>4} {:<5} {}",
65 "#".dimmed(),
66 "ID".dimmed(),
67 "Title".dimmed(),
68 "Status".dimmed(),
69 "Cplx".dimmed(),
70 "Pri".dimmed(),
71 "Agent".dimmed()
72 );
73 println!("{}", "─".repeat(90).dimmed());
74
75 let mut sorted_tasks = phase.tasks.clone();
77 sorted_tasks.sort_by(|a, b| natural_sort_ids(&a.id, &b.id));
78
79 for (idx, task) in sorted_tasks.iter().enumerate() {
80 let title = if task.title.len() > 30 {
81 format!("{}...", &task.title[..27])
82 } else {
83 task.title.clone()
84 };
85
86 println!(
87 "{:>4} {:<11} {:<32} {:<14} {:>4} {:<5} {}",
88 (idx + 1).to_string().dimmed(),
89 format_task_id(&task.id).cyan(),
90 title,
91 format_status(&task.status),
92 task.complexity,
93 format_priority(&task.priority),
94 format_agent_type(&task.agent_type).dimmed()
95 );
96 }
97
98 println!();
99 println!("{} {} tasks", "Total:".dimmed(), phase.tasks.len());
100}
101
102pub fn run(
103 project_root: Option<PathBuf>,
104 status_filter: Option<&str>,
105 tag: Option<&str>,
106 json_output: bool,
107 verbose: bool,
108) -> Result<()> {
109 let storage = Storage::new(project_root);
110
111 let phase_tag = resolve_group_tag(&storage, tag, true)?;
112 let tasks = storage.load_tasks()?;
113 let phase = tasks
114 .get(&phase_tag)
115 .ok_or_else(|| anyhow::anyhow!("Phase '{}' not found", phase_tag))?;
116
117 let filter_status = status_filter
118 .map(|s| {
119 TaskStatus::from_str(s).ok_or_else(|| {
120 anyhow::anyhow!("Invalid status: {}. Valid: {:?}", s, TaskStatus::all())
121 })
122 })
123 .transpose()?;
124
125 let filtered_phase = if filter_status.is_some() {
126 let filtered_tasks: Vec<_> = phase
127 .tasks
128 .iter()
129 .filter(|t| {
130 filter_status
131 .as_ref()
132 .map(|fs| t.status == *fs)
133 .unwrap_or(true)
134 })
135 .cloned()
136 .collect();
137
138 let mut filtered = Phase::new(phase.name.clone());
139 filtered.tasks = filtered_tasks;
140 filtered
141 } else {
142 phase.clone()
143 };
144
145 if filtered_phase.tasks.is_empty() {
146 if json_output {
147 println!("[]");
148 } else if verbose {
149 println!("# SCUD Graph v1");
150 println!("# Phase: {}", phase_tag);
151 println!();
152 println!("@nodes");
153 println!("# id | title | status | complexity | priority");
154 println!("# (no tasks)");
155 } else {
156 println!("{} {}\n", "Phase:".blue().bold(), phase_tag.cyan());
157 println!("{}", "(no tasks)".dimmed());
158 }
159 return Ok(());
160 }
161
162 if json_output {
163 let json = serde_json::to_string_pretty(&filtered_phase.tasks)?;
164 println!("{}", json);
165 } else if verbose {
166 let scg = serialize_scg(&filtered_phase);
168 print!("{}", scg);
169 } else {
170 print_human_readable(&filtered_phase, &phase_tag);
172 }
173
174 Ok(())
175}