Skip to main content

rustyclaw_core/tasks/
display.rs

1//! Task display β€” icons, indicators, and formatting.
2
3use super::model::{Task, TaskStatus};
4
5/// Icon representation for task status.
6#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum TaskIcon {
8    /// ⏳ Pending/waiting
9    Pending,
10    /// πŸ”„ Running (spinning)
11    Running,
12    /// ⏸️ Paused
13    Paused,
14    /// βœ… Completed
15    Completed,
16    /// ❌ Failed
17    Failed,
18    /// 🚫 Cancelled
19    Cancelled,
20    /// πŸ’¬ Waiting for input
21    WaitingInput,
22    /// πŸ“¦ Background
23    Background,
24}
25
26impl TaskIcon {
27    /// Get from task status.
28    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    /// Get emoji representation.
42    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    /// Get ANSI color code for terminal.
56    pub fn ansi_color(&self) -> &'static str {
57        match self {
58            Self::Pending => "\x1b[33m",      // Yellow
59            Self::Running => "\x1b[36m",      // Cyan
60            Self::Paused => "\x1b[33m",       // Yellow
61            Self::Completed => "\x1b[32m",    // Green
62            Self::Failed => "\x1b[31m",       // Red
63            Self::Cancelled => "\x1b[90m",    // Gray
64            Self::WaitingInput => "\x1b[35m", // Magenta
65            Self::Background => "\x1b[34m",   // Blue
66        }
67    }
68
69    /// Get unicode spinner frame (for Running state).
70    pub fn spinner_frame(frame: usize) -> &'static str {
71        const FRAMES: &[&str] = &["β ‹", "β ™", "β Ή", "β Έ", "β Ό", "β ΄", "β ¦", "β §", "β ‡", "⠏"];
72        FRAMES[frame % FRAMES.len()]
73    }
74
75    /// Get progress bar character.
76    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/// Indicator style for chat display.
88#[derive(Debug, Clone)]
89pub struct TaskIndicator {
90    /// The icon to show
91    pub icon: TaskIcon,
92
93    /// Short label
94    pub label: String,
95
96    /// Optional progress bar (width in chars)
97    pub progress_bar: Option<String>,
98
99    /// Optional time remaining
100    pub eta: Option<String>,
101
102    /// Whether this is the foreground task
103    pub foreground: bool,
104}
105
106impl TaskIndicator {
107    /// Create from a task.
108    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    /// Format as a compact inline indicator for chat.
130    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    /// Format as a badge for status bar.
145    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
154/// Format a progress bar.
155pub 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
167/// Format a duration for display.
168pub 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
180/// Format a task status for display.
181pub 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
239/// Format a set of tasks as icon bar (for status display).
240pub 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
252/// Format tasks for chat indicator line.
253pub 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}