cargo_hammerwork/utils/display.rs
1//! Display formatting utilities for the Hammerwork CLI.
2//!
3//! This module provides table formatting and display utilities for presenting
4//! job information, statistics, and other data in a user-friendly format.
5//!
6//! # Examples
7//!
8//! ## Creating a Job Table
9//!
10//! ```rust
11//! use cargo_hammerwork::utils::display::JobTable;
12//!
13//! let mut table = JobTable::new();
14//! table.add_job_row(
15//! "550e8400-e29b-41d4-a716-446655440000",
16//! "email",
17//! "pending",
18//! "high",
19//! 0,
20//! "2024-01-01 10:00:00",
21//! "2024-01-01 10:05:00"
22//! );
23//!
24//! // Display the table
25//! println!("{}", table);
26//! ```
27//!
28//! ## Creating a Statistics Table
29//!
30//! ```rust
31//! use cargo_hammerwork::utils::display::StatsTable;
32//!
33//! let mut stats = StatsTable::new();
34//! stats.add_stats_row("pending", "normal", 42);
35//! stats.add_stats_row("running", "high", 5);
36//! stats.add_stats_row("completed", "normal", 1337);
37//!
38//! println!("{}", stats);
39//! ```
40//!
41//! ## Formatting Helpers
42//!
43//! ```rust
44//! use cargo_hammerwork::utils::display::{format_duration, format_size};
45//!
46//! // Format durations
47//! assert_eq!(format_duration(Some(45)), "45s");
48//! assert_eq!(format_duration(Some(125)), "2m 5s");
49//! assert_eq!(format_duration(Some(3661)), "1h 1m");
50//! assert_eq!(format_duration(None), "N/A");
51//!
52//! // Format sizes
53//! assert_eq!(format_size(Some(512)), "512B");
54//! assert_eq!(format_size(Some(2048)), "2.0KB");
55//! assert_eq!(format_size(Some(1_048_576)), "1.0MB");
56//! assert_eq!(format_size(None), "N/A");
57//! ```
58
59use comfy_table::Table;
60use std::fmt;
61
62/// Create a new basic table with default styling.
63///
64/// This is a convenience function for creating simple tables in CLI commands.
65pub fn create_table() -> Table {
66 Table::new()
67}
68
69/// Table formatter for displaying job information.
70///
71/// This struct creates formatted tables with job details including
72/// status icons, priority indicators, and truncated IDs for readability.
73///
74/// # Examples
75///
76/// ```rust
77/// use cargo_hammerwork::utils::display::JobTable;
78///
79/// let mut table = JobTable::new();
80///
81/// // Add multiple jobs
82/// table.add_job_row(
83/// "job-id-1", "email", "pending", "normal", 0,
84/// "2024-01-01 10:00:00", "2024-01-01 10:00:00"
85/// );
86/// table.add_job_row(
87/// "job-id-2", "data-processing", "running", "high", 1,
88/// "2024-01-01 09:55:00", "2024-01-01 10:00:00"
89/// );
90///
91/// // The table will display with color-coded status and priority
92/// ```
93pub struct JobTable {
94 table: Table,
95}
96
97impl Default for JobTable {
98 fn default() -> Self {
99 Self::new()
100 }
101}
102
103impl JobTable {
104 /// Create a new job table with predefined headers.
105 ///
106 /// Headers include: ID, Queue, Status, Priority, Attempts, Created At, Scheduled At
107 pub fn new() -> Self {
108 let mut table = Table::new();
109 table.set_header(vec![
110 "ID",
111 "Queue",
112 "Status",
113 "Priority",
114 "Attempts",
115 "Created At",
116 "Scheduled At",
117 ]);
118 Self { table }
119 }
120
121 /// Add a job row to the table.
122 ///
123 /// The method automatically:
124 /// - Truncates job IDs to 8 characters for readability
125 /// - Adds status icons and colors (🟡 pending, 🔵 running, 🟢 completed, etc.)
126 /// - Adds priority icons (🚨 critical, ⚡ high, 📝 normal, etc.)
127 ///
128 /// # Arguments
129 ///
130 /// * `id` - Job UUID
131 /// * `queue_name` - Name of the queue
132 /// * `status` - Job status (pending, running, completed, failed, dead, retrying)
133 /// * `priority` - Job priority (critical, high, normal, low, background)
134 /// * `attempts` - Number of execution attempts
135 /// * `created_at` - Creation timestamp
136 /// * `scheduled_at` - Scheduled execution timestamp
137 #[allow(clippy::too_many_arguments)]
138 pub fn add_job_row(
139 &mut self,
140 id: &str,
141 queue_name: &str,
142 status: &str,
143 priority: &str,
144 attempts: i32,
145 created_at: &str,
146 scheduled_at: &str,
147 ) {
148 let status_colored = match status {
149 "pending" => format!("🟡 {}", status),
150 "running" => format!("🔵 {}", status),
151 "completed" => format!("🟢 {}", status),
152 "failed" => format!("🔴 {}", status),
153 "dead" => format!("💀 {}", status),
154 "retrying" => format!("🟠 {}", status),
155 _ => status.to_string(),
156 };
157
158 let priority_colored = match priority {
159 "critical" => format!("🚨 {}", priority),
160 "high" => format!("⚡ {}", priority),
161 "normal" => format!("📝 {}", priority),
162 "low" => format!("🐌 {}", priority),
163 "background" => format!("💤 {}", priority),
164 _ => priority.to_string(),
165 };
166
167 self.table.add_row(vec![
168 &id[..8.min(id.len())],
169 queue_name,
170 &status_colored,
171 &priority_colored,
172 &attempts.to_string(),
173 created_at,
174 scheduled_at,
175 ]);
176 }
177}
178
179impl fmt::Display for JobTable {
180 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181 write!(f, "{}", self.table)
182 }
183}
184
185/// Table formatter for displaying job statistics.
186///
187/// This struct creates formatted tables showing job counts by status and priority,
188/// with visual indicators for easy scanning.
189///
190/// # Examples
191///
192/// ```rust
193/// use cargo_hammerwork::utils::display::StatsTable;
194///
195/// let mut stats = StatsTable::new();
196/// stats.add_stats_row("pending", "normal", 100);
197/// stats.add_stats_row("running", "high", 10);
198/// stats.add_stats_row("failed", "critical", 2);
199///
200/// // Display shows icons: 🟡 pending, 🔵 running, 🔴 failed
201/// ```
202pub struct StatsTable {
203 table: Table,
204}
205
206impl Default for StatsTable {
207 fn default() -> Self {
208 Self::new()
209 }
210}
211
212impl StatsTable {
213 pub fn new() -> Self {
214 let mut table = Table::new();
215 table.set_header(vec!["Status", "Priority", "Count"]);
216 Self { table }
217 }
218
219 pub fn add_stats_row(&mut self, status: &str, priority: &str, count: i64) {
220 let status_icon = match status {
221 "pending" => "🟡",
222 "running" => "🔵",
223 "completed" => "🟢",
224 "failed" => "🔴",
225 "dead" => "💀",
226 "retrying" => "🟠",
227 _ => "❓",
228 };
229
230 let priority_icon = match priority {
231 "critical" => "🚨",
232 "high" => "⚡",
233 "normal" => "📝",
234 "low" => "🐌",
235 "background" => "💤",
236 _ => "❓",
237 };
238
239 self.table.add_row(vec![
240 &format!("{} {}", status_icon, status),
241 &format!("{} {}", priority_icon, priority),
242 &count.to_string(),
243 ]);
244 }
245}
246
247impl fmt::Display for StatsTable {
248 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249 write!(f, "{}", self.table)
250 }
251}
252
253/// Format a duration in seconds to a human-readable string.
254///
255/// # Examples
256///
257/// ```rust
258/// use cargo_hammerwork::utils::display::format_duration;
259///
260/// assert_eq!(format_duration(Some(30)), "30s");
261/// assert_eq!(format_duration(Some(90)), "1m 30s");
262/// assert_eq!(format_duration(Some(3665)), "1h 1m");
263/// assert_eq!(format_duration(Some(7200)), "2h 0m");
264/// assert_eq!(format_duration(None), "N/A");
265/// ```
266pub fn format_duration(seconds: Option<i64>) -> String {
267 match seconds {
268 Some(secs) if secs < 60 => format!("{}s", secs),
269 Some(secs) if secs < 3600 => format!("{}m {}s", secs / 60, secs % 60),
270 Some(secs) => format!("{}h {}m", secs / 3600, (secs % 3600) / 60),
271 None => "N/A".to_string(),
272 }
273}
274
275/// Format a byte size to a human-readable string.
276///
277/// # Examples
278///
279/// ```rust
280/// use cargo_hammerwork::utils::display::format_size;
281///
282/// assert_eq!(format_size(Some(100)), "100B");
283/// assert_eq!(format_size(Some(1024)), "1.0KB");
284/// assert_eq!(format_size(Some(1536)), "1.5KB");
285/// assert_eq!(format_size(Some(1048576)), "1.0MB");
286/// assert_eq!(format_size(Some(1073741824)), "1.0GB");
287/// assert_eq!(format_size(None), "N/A");
288/// ```
289pub fn format_size(bytes: Option<i64>) -> String {
290 match bytes {
291 Some(b) if b < 1024 => format!("{}B", b),
292 Some(b) if b < 1024 * 1024 => format!("{:.1}KB", b as f64 / 1024.0),
293 Some(b) if b < 1024 * 1024 * 1024 => format!("{:.1}MB", b as f64 / (1024.0 * 1024.0)),
294 Some(b) => format!("{:.1}GB", b as f64 / (1024.0 * 1024.0 * 1024.0)),
295 None => "N/A".to_string(),
296 }
297}