1use super::model::{Task, TaskStatus};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum TaskIcon {
8 Pending,
10 Running,
12 Paused,
14 Completed,
16 Failed,
18 Cancelled,
20 WaitingInput,
22 Background,
24}
25
26impl TaskIcon {
27 pub fn from_status(status: &TaskStatus) -> Self {
29 match status {
30 TaskStatus::Pending => Self::Pending,
31 TaskStatus::Running { .. } => Self::Running,
32 TaskStatus::Background { .. } => Self::Background,
33 TaskStatus::Paused { .. } => Self::Paused,
34 TaskStatus::Completed { .. } => Self::Completed,
35 TaskStatus::Failed { .. } => Self::Failed,
36 TaskStatus::Cancelled => Self::Cancelled,
37 TaskStatus::WaitingForInput { .. } => Self::WaitingInput,
38 }
39 }
40
41 pub fn emoji(&self) -> &'static str {
43 match self {
44 Self::Pending => "β³",
45 Self::Running => "π",
46 Self::Paused => "βΈοΈ",
47 Self::Completed => "β
",
48 Self::Failed => "β",
49 Self::Cancelled => "π«",
50 Self::WaitingInput => "π¬",
51 Self::Background => "π¦",
52 }
53 }
54
55 pub fn ansi_color(&self) -> &'static str {
57 match self {
58 Self::Pending => "\x1b[33m", Self::Running => "\x1b[36m", Self::Paused => "\x1b[33m", Self::Completed => "\x1b[32m", Self::Failed => "\x1b[31m", Self::Cancelled => "\x1b[90m", Self::WaitingInput => "\x1b[35m", Self::Background => "\x1b[34m", }
67 }
68
69 pub fn spinner_frame(frame: usize) -> &'static str {
71 const FRAMES: &[&str] = &["β ", "β ", "β Ή", "β Έ", "β Ό", "β ΄", "β ¦", "β §", "β ", "β "];
72 FRAMES[frame % FRAMES.len()]
73 }
74
75 pub fn progress_char(filled: bool) -> &'static str {
77 if filled { "β" } else { "β" }
78 }
79}
80
81impl std::fmt::Display for TaskIcon {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 write!(f, "{}", self.emoji())
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct TaskIndicator {
90 pub icon: TaskIcon,
92
93 pub label: String,
95
96 pub progress_bar: Option<String>,
98
99 pub eta: Option<String>,
101
102 pub foreground: bool,
104}
105
106impl TaskIndicator {
107 pub fn from_task(task: &Task) -> Self {
109 let icon = TaskIcon::from_status(&task.status);
110 let label = task.display_label();
111
112 let progress_bar = task.status.progress().map(|p| format_progress_bar(p, 10));
113
114 let eta = if let TaskStatus::Running { .. } | TaskStatus::Background { .. } = &task.status {
115 task.elapsed().map(|d| format_duration(d))
116 } else {
117 None
118 };
119
120 Self {
121 icon,
122 label,
123 progress_bar,
124 eta,
125 foreground: task.status.is_foreground(),
126 }
127 }
128
129 pub fn inline(&self) -> String {
131 let mut s = format!("{} {}", self.icon, self.label);
132
133 if let Some(ref bar) = self.progress_bar {
134 s.push_str(&format!(" {}", bar));
135 }
136
137 if let Some(ref eta) = self.eta {
138 s.push_str(&format!(" ({})", eta));
139 }
140
141 s
142 }
143
144 pub fn badge(&self) -> String {
146 if let Some(ref bar) = self.progress_bar {
147 format!("{}{}", self.icon, bar)
148 } else {
149 format!("{}", self.icon)
150 }
151 }
152}
153
154pub fn format_progress_bar(fraction: f32, width: usize) -> String {
156 let filled = (fraction * width as f32).round() as usize;
157 let empty = width.saturating_sub(filled);
158
159 format!(
160 "[{}{}] {}%",
161 "β".repeat(filled),
162 "β".repeat(empty),
163 (fraction * 100.0).round() as u32
164 )
165}
166
167pub fn format_duration(d: std::time::Duration) -> String {
169 let secs = d.as_secs();
170
171 if secs < 60 {
172 format!("{}s", secs)
173 } else if secs < 3600 {
174 format!("{}m {}s", secs / 60, secs % 60)
175 } else {
176 format!("{}h {}m", secs / 3600, (secs % 3600) / 60)
177 }
178}
179
180pub fn format_task_status(task: &Task) -> String {
182 let icon = TaskIcon::from_status(&task.status);
183 let label = task.display_label();
184
185 let status_msg = match &task.status {
186 TaskStatus::Pending => "Waiting to start".to_string(),
187 TaskStatus::Running { message, progress } => {
188 let mut s = "Running".to_string();
189 if let Some(p) = progress {
190 s.push_str(&format!(" ({}%)", (*p * 100.0).round() as u32));
191 }
192 if let Some(m) = message {
193 s.push_str(&format!(": {}", m));
194 }
195 s
196 }
197 TaskStatus::Background { message, progress } => {
198 let mut s = "Background".to_string();
199 if let Some(p) = progress {
200 s.push_str(&format!(" ({}%)", (*p * 100.0).round() as u32));
201 }
202 if let Some(m) = message {
203 s.push_str(&format!(": {}", m));
204 }
205 s
206 }
207 TaskStatus::Paused { reason } => {
208 if let Some(r) = reason {
209 format!("Paused: {}", r)
210 } else {
211 "Paused".to_string()
212 }
213 }
214 TaskStatus::Completed { summary, .. } => {
215 if let Some(s) = summary {
216 format!("Completed: {}", s)
217 } else {
218 "Completed".to_string()
219 }
220 }
221 TaskStatus::Failed { error, retryable } => {
222 let retry = if *retryable { " (retryable)" } else { "" };
223 format!("Failed{}: {}", retry, error)
224 }
225 TaskStatus::Cancelled => "Cancelled".to_string(),
226 TaskStatus::WaitingForInput { prompt } => {
227 format!("Waiting: {}", prompt)
228 }
229 };
230
231 let elapsed = task
232 .elapsed()
233 .map(|d| format!(" [{}]", format_duration(d)))
234 .unwrap_or_default();
235
236 format!("{} {} β {}{}", icon, label, status_msg, elapsed)
237}
238
239pub fn format_task_icons(tasks: &[Task]) -> String {
241 if tasks.is_empty() {
242 return String::new();
243 }
244
245 tasks
246 .iter()
247 .map(|t| TaskIcon::from_status(&t.status).emoji())
248 .collect::<Vec<_>>()
249 .join("")
250}
251
252pub fn format_task_indicators(tasks: &[Task], max_display: usize) -> String {
254 if tasks.is_empty() {
255 return String::new();
256 }
257
258 let active: Vec<_> = tasks
259 .iter()
260 .filter(|t| !t.status.is_terminal())
261 .take(max_display)
262 .collect();
263
264 if active.is_empty() {
265 return String::new();
266 }
267
268 let indicators: Vec<_> = active
269 .iter()
270 .map(|t| TaskIndicator::from_task(t).badge())
271 .collect();
272
273 let remaining = tasks
274 .iter()
275 .filter(|t| !t.status.is_terminal())
276 .count()
277 .saturating_sub(max_display);
278
279 if remaining > 0 {
280 format!("{} +{}", indicators.join(" "), remaining)
281 } else {
282 indicators.join(" ")
283 }
284}