1use crate::status::{AttentionItem, InProgressItem, ReadyItem, StatusData, TodayActivity};
6use crate::ui;
7
8pub fn format_regular_status(data: &StatusData) -> String {
10 let mut output = vec![
11 ui::colors::heading("Chant Status").to_string(),
12 ui::format::separator(12),
13 String::new(),
14 format_counts(&data.counts),
15 String::new(),
16 ];
17
18 if data.today.completed > 0 || data.today.started > 0 || data.today.created > 0 {
20 output.push(ui::colors::heading("Today").to_string());
21 output.push(ui::format::separator(5));
22 output.push(format_today(&data.today));
23 output.push(String::new());
24 }
25
26 if !data.attention.is_empty() {
28 output.push(ui::colors::heading("Attention").to_string());
29 output.push(ui::format::separator(9));
30 for item in &data.attention {
31 output.push(format_attention_item(item));
32 }
33 output.push(String::new());
34 }
35
36 if !data.in_progress.is_empty() {
38 output.push(ui::colors::heading("In Progress").to_string());
39 output.push(ui::format::separator(11));
40 for item in &data.in_progress {
41 output.push(format_in_progress_item(item));
42 }
43 output.push(String::new());
44 }
45
46 output.push(ui::colors::heading(&format!("Ready ({})", data.ready_count)).to_string());
48 output.push(ui::format::separator(6));
49 if data.ready_count == 0 {
50 output.push(ui::colors::secondary(" (no specs ready)").to_string());
51 } else {
52 for item in &data.ready {
53 output.push(format_ready_item(item));
54 }
55 if data.ready_count > 5 {
56 let remaining = data.ready_count - 5;
57 output
58 .push(ui::colors::secondary(&format!(" ... and {} more", remaining)).to_string());
59 }
60 }
61
62 output.join("\n")
63}
64
65fn format_counts(counts: &std::collections::HashMap<String, usize>) -> String {
67 let pending = counts.get("pending").copied().unwrap_or(0);
68 let in_progress = counts.get("in_progress").copied().unwrap_or(0);
69 let completed = counts.get("completed").copied().unwrap_or(0);
70 let failed = counts.get("failed").copied().unwrap_or(0);
71 let blocked = counts.get("blocked").copied().unwrap_or(0);
72 let ready = counts.get("ready").copied().unwrap_or(0);
73
74 format!(
75 " {:<12} {}\n {:<12} {}\n {:<12} {}\n {:<12} {}\n {:<12} {}\n {:<12} {}",
76 "Pending:",
77 pending,
78 "Ready:",
79 ready,
80 "In Progress:",
81 in_progress,
82 "Completed:",
83 completed,
84 "Failed:",
85 failed,
86 "Blocked:",
87 blocked,
88 )
89}
90
91fn format_today(today: &TodayActivity) -> String {
93 let mut parts = Vec::new();
94
95 if today.completed > 0 {
96 parts.push(ui::colors::success(&format!("+{} completed", today.completed)).to_string());
97 }
98 if today.started > 0 {
99 parts.push(ui::colors::warning(&format!("+{} started", today.started)).to_string());
100 }
101 if today.created > 0 {
102 parts.push(ui::colors::info(&format!("+{} created", today.created)).to_string());
103 }
104
105 if parts.is_empty() {
106 ui::colors::secondary(" (no activity today)").to_string()
107 } else {
108 format!(" {}", parts.join(", "))
109 }
110}
111
112fn format_attention_item(item: &AttentionItem) -> String {
114 let symbol = ui::attention_symbol(&item.status);
115
116 let title = item.title.as_deref().unwrap_or("(untitled)");
117 let truncated_title = ui::format::truncate_title(title, 60);
118
119 format!(
120 " {} {} {} ({})",
121 symbol,
122 ui::colors::identifier(&item.id),
123 truncated_title,
124 ui::colors::secondary(&item.ago)
125 )
126}
127
128fn format_in_progress_item(item: &InProgressItem) -> String {
130 let title = item.title.as_deref().unwrap_or("(untitled)");
131 let truncated_title = ui::format::truncate_title(title, 60);
132
133 let elapsed_str = ui::format::elapsed_minutes(item.elapsed_minutes);
134
135 format!(
136 " {} {} ({})",
137 ui::colors::identifier(&item.id),
138 truncated_title,
139 ui::colors::secondary(&elapsed_str)
140 )
141}
142
143fn format_ready_item(item: &ReadyItem) -> String {
145 let title = item.title.as_deref().unwrap_or("(untitled)");
146 let truncated_title = ui::format::truncate_title(title, 60);
147
148 format!(" {} {}", ui::colors::identifier(&item.id), truncated_title)
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use std::collections::HashMap;
155
156 #[test]
157 fn test_format_counts() {
158 let mut counts = HashMap::new();
159 counts.insert("pending".to_string(), 5);
160 counts.insert("in_progress".to_string(), 2);
161 counts.insert("completed".to_string(), 10);
162 counts.insert("failed".to_string(), 1);
163 counts.insert("blocked".to_string(), 0);
164 counts.insert("ready".to_string(), 3);
165
166 let result = format_counts(&counts);
167 assert!(result.contains("Pending:"));
168 assert!(result.contains("5"));
169 assert!(result.contains("Ready:"));
170 assert!(result.contains("3"));
171 }
172
173 #[test]
174 fn test_format_today_all_activity() {
175 let today = TodayActivity {
176 completed: 2,
177 started: 1,
178 created: 3,
179 };
180
181 let result = format_today(&today);
182 assert!(result.contains("+2 completed"));
183 assert!(result.contains("+1 started"));
184 assert!(result.contains("+3 created"));
185 }
186
187 #[test]
188 fn test_format_today_no_activity() {
189 let today = TodayActivity {
190 completed: 0,
191 started: 0,
192 created: 0,
193 };
194
195 let result = format_today(&today);
196 assert!(result.contains("no activity"));
197 }
198
199 #[test]
200 fn test_format_regular_status_empty() {
201 let data = StatusData::default();
202 let result = format_regular_status(&data);
203
204 assert!(result.contains("Chant Status"));
205 assert!(result.contains("Ready (0)"));
206 assert!(result.contains("no specs ready"));
207 assert!(!result.contains("Attention\n─────────"));
209 assert!(result.contains("In Progress:"));
211 assert!(!result.contains("In Progress\n───────────"));
212 }
213}