ipfrs_cli/
output.rs

1//! Output formatting and colored output utilities for IPFRS CLI
2//!
3//! This module provides utilities for formatting CLI output with colors,
4//! tables, and different output modes (text, JSON, compact).
5//!
6//! # Examples
7//!
8//! ```rust
9//! use ipfrs_cli::output::{OutputStyle, format_bytes, format_bytes_detailed};
10//!
11//! // Create output style
12//! let style = OutputStyle::new(true, "text");
13//!
14//! // Format file sizes
15//! let size = format_bytes(1048576);
16//! assert_eq!(size, "1.00 MB");
17//!
18//! let detailed = format_bytes_detailed(1234567);
19//! assert_eq!(detailed, "1.18 MB (1234567 bytes)");
20//! ```
21
22#![allow(dead_code)]
23
24use colored::Colorize;
25use std::io::{self, Write};
26
27/// Check if stdout is a TTY (terminal)
28///
29/// # Examples
30///
31/// ```rust
32/// use ipfrs_cli::output::is_tty;
33///
34/// // In tests, this typically returns false
35/// let tty = is_tty();
36/// assert!(tty == true || tty == false); // Platform dependent
37/// ```
38pub fn is_tty() -> bool {
39    atty::is(atty::Stream::Stdout)
40}
41
42/// Output style configuration for controlling formatting and colors
43///
44/// # Examples
45///
46/// ```rust
47/// use ipfrs_cli::output::OutputStyle;
48///
49/// // Create style with colors enabled
50/// let style = OutputStyle::new(true, "text");
51/// assert_eq!(style.format, "text");
52///
53/// // Create compact style
54/// let compact = OutputStyle::new(false, "compact");
55/// assert!(compact.is_compact());
56///
57/// // JSON format disables colors
58/// let json = OutputStyle::new(true, "json");
59/// assert_eq!(json.format, "json");
60/// ```
61pub struct OutputStyle {
62    /// Enable colored output
63    pub color: bool,
64    /// Output format (text, json, compact)
65    pub format: String,
66    /// Compact mode (minimal output)
67    pub compact: bool,
68    /// Quiet mode (suppress non-essential output)
69    pub quiet: bool,
70}
71
72impl Default for OutputStyle {
73    fn default() -> Self {
74        Self {
75            color: is_tty(),
76            format: "text".to_string(),
77            compact: false,
78            quiet: false,
79        }
80    }
81}
82
83impl OutputStyle {
84    /// Create new output style with color control
85    pub fn new(color: bool, format: &str) -> Self {
86        // Disable colors if not TTY or if format is JSON
87        let effective_color = color && is_tty() && format != "json" && format != "compact";
88        let compact = format == "compact";
89        Self {
90            color: effective_color,
91            format: format.to_string(),
92            compact,
93            quiet: false,
94        }
95    }
96
97    /// Create new output style with quiet mode
98    pub fn with_quiet(color: bool, format: &str, quiet: bool) -> Self {
99        let mut style = Self::new(color, format);
100        style.quiet = quiet;
101        style
102    }
103
104    /// Check if compact mode is enabled
105    pub fn is_compact(&self) -> bool {
106        self.compact || self.format == "compact"
107    }
108
109    /// Check if quiet mode is enabled
110    pub fn is_quiet(&self) -> bool {
111        self.quiet
112    }
113}
114
115/// Print a success message
116pub fn success(msg: &str) {
117    if is_tty() {
118        println!("{} {}", "✓".green().bold(), msg.green());
119    } else {
120        println!("{}", msg);
121    }
122}
123
124/// Print an error message
125pub fn error(msg: &str) {
126    if is_tty() {
127        eprintln!("{} {}", "✗".red().bold(), msg.red());
128    } else {
129        eprintln!("error: {}", msg);
130    }
131}
132
133/// Print a warning message
134pub fn warning(msg: &str) {
135    if is_tty() {
136        eprintln!("{} {}", "!".yellow().bold(), msg.yellow());
137    } else {
138        eprintln!("warning: {}", msg);
139    }
140}
141
142/// Print an info message
143pub fn info(msg: &str) {
144    if is_tty() {
145        println!("{} {}", "ℹ".blue().bold(), msg);
146    } else {
147        println!("{}", msg);
148    }
149}
150
151/// Print a CID (content identifier) with highlighting
152pub fn print_cid(label: &str, cid: &str) {
153    if is_tty() {
154        println!("{}: {}", label, cid.cyan().bold());
155    } else {
156        println!("{}: {}", label, cid);
157    }
158}
159
160/// Print a key-value pair
161pub fn print_kv(key: &str, value: &str) {
162    if is_tty() {
163        println!("  {}: {}", key.dimmed(), value);
164    } else {
165        println!("  {}: {}", key, value);
166    }
167}
168
169/// Print a header/title
170pub fn print_header(title: &str) {
171    if is_tty() {
172        println!("{}", title.bold().underline());
173    } else {
174        println!("{}", title);
175        println!("{}", "=".repeat(title.len()));
176    }
177}
178
179/// Print a section header
180pub fn print_section(title: &str) {
181    if is_tty() {
182        println!("\n{}", title.bold());
183    } else {
184        println!("\n{}", title);
185    }
186}
187
188/// Format bytes as human-readable size
189pub fn format_bytes(bytes: u64) -> String {
190    const KB: u64 = 1024;
191    const MB: u64 = KB * 1024;
192    const GB: u64 = MB * 1024;
193    const TB: u64 = GB * 1024;
194
195    if bytes >= TB {
196        format!("{:.2} TB", bytes as f64 / TB as f64)
197    } else if bytes >= GB {
198        format!("{:.2} GB", bytes as f64 / GB as f64)
199    } else if bytes >= MB {
200        format!("{:.2} MB", bytes as f64 / MB as f64)
201    } else if bytes >= KB {
202        format!("{:.2} KB", bytes as f64 / KB as f64)
203    } else {
204        format!("{} B", bytes)
205    }
206}
207
208/// Format bytes with both human-readable and exact value
209pub fn format_bytes_detailed(bytes: u64) -> String {
210    if bytes >= 1024 {
211        format!("{} ({} bytes)", format_bytes(bytes), bytes)
212    } else {
213        format!("{} bytes", bytes)
214    }
215}
216
217/// Print a list item
218pub fn print_list_item(item: &str) {
219    if is_tty() {
220        println!("  {} {}", "•".dimmed(), item);
221    } else {
222        println!("  - {}", item);
223    }
224}
225
226/// Print a numbered list item
227pub fn print_numbered_item(num: usize, item: &str) {
228    if is_tty() {
229        println!("  {}. {}", num.to_string().dimmed(), item);
230    } else {
231        println!("  {}. {}", num, item);
232    }
233}
234
235/// Table printer for formatted output
236pub struct TablePrinter {
237    headers: Vec<String>,
238    rows: Vec<Vec<String>>,
239    column_widths: Vec<usize>,
240}
241
242impl TablePrinter {
243    /// Create a new table with headers
244    pub fn new(headers: Vec<&str>) -> Self {
245        let headers: Vec<String> = headers.iter().map(|s| s.to_string()).collect();
246        let column_widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
247        Self {
248            headers,
249            rows: Vec::new(),
250            column_widths,
251        }
252    }
253
254    /// Add a row to the table
255    pub fn add_row(&mut self, row: Vec<&str>) {
256        let row: Vec<String> = row.iter().map(|s| s.to_string()).collect();
257        for (i, cell) in row.iter().enumerate() {
258            if i < self.column_widths.len() {
259                self.column_widths[i] = self.column_widths[i].max(cell.len());
260            }
261        }
262        self.rows.push(row);
263    }
264
265    /// Print the table to stdout
266    pub fn print(&self) {
267        let color = is_tty();
268
269        // Print header
270        let header_line: String = self
271            .headers
272            .iter()
273            .enumerate()
274            .map(|(i, h)| format!("{:width$}", h, width = self.column_widths[i]))
275            .collect::<Vec<_>>()
276            .join("  ");
277
278        if color {
279            println!("{}", header_line.bold());
280        } else {
281            println!("{}", header_line);
282        }
283
284        // Print separator
285        let separator: String = self
286            .column_widths
287            .iter()
288            .map(|&w| "-".repeat(w))
289            .collect::<Vec<_>>()
290            .join("  ");
291
292        if color {
293            println!("{}", separator.dimmed());
294        } else {
295            println!("{}", separator);
296        }
297
298        // Print rows
299        for row in &self.rows {
300            let row_line: String = row
301                .iter()
302                .enumerate()
303                .map(|(i, cell)| {
304                    let width = self.column_widths.get(i).copied().unwrap_or(cell.len());
305                    format!("{:width$}", cell, width = width)
306                })
307                .collect::<Vec<_>>()
308                .join("  ");
309            println!("{}", row_line);
310        }
311    }
312}
313
314/// Write raw bytes to stdout (for binary output)
315pub fn write_raw(data: &[u8]) -> io::Result<()> {
316    let stdout = io::stdout();
317    let mut handle = stdout.lock();
318    handle.write_all(data)?;
319    handle.flush()
320}
321
322/// Print in compact mode (minimal output)
323pub fn compact_print(key: &str, value: &str) {
324    println!("{}:{}", key, value);
325}
326
327/// Print CID in compact mode
328pub fn compact_cid(cid: &str) {
329    println!("{}", cid);
330}
331
332/// Print list in compact mode (one item per line)
333pub fn compact_list(items: &[String]) {
334    for item in items {
335        println!("{}", item);
336    }
337}
338
339/// Print key-value pairs in compact mode
340pub fn compact_kv_pairs(pairs: &[(&str, &str)]) {
341    for (key, value) in pairs {
342        println!("{}:{}", key, value);
343    }
344}
345
346/// Print troubleshooting hint for common errors
347///
348/// This function provides helpful troubleshooting messages for common error scenarios
349pub fn troubleshooting_hint(error_type: &str) {
350    let hint = match error_type {
351        "daemon_not_running" => {
352            "The IPFRS daemon is not running.\n\
353             To start the daemon, run: ipfrs daemon start\n\
354             Or run in foreground: ipfrs daemon"
355        }
356        "daemon_already_running" => {
357            "The IPFRS daemon is already running.\n\
358             To stop it, run: ipfrs daemon stop\n\
359             To check status: ipfrs daemon status"
360        }
361        "repo_not_initialized" => {
362            "IPFRS repository not initialized.\n\
363             To initialize a repository, run: ipfrs init\n\
364             Or specify a custom directory: ipfrs init -d /path/to/repo"
365        }
366        "connection_failed" => {
367            "Failed to connect to peer.\n\
368             Troubleshooting steps:\n\
369             1. Check if the peer is online\n\
370             2. Verify the multiaddr format is correct\n\
371             3. Check your network connection\n\
372             4. Ensure firewall allows IPFRS connections"
373        }
374        "cid_not_found" => {
375            "Content not found.\n\
376             This could mean:\n\
377             1. The CID is incorrect or malformed\n\
378             2. The content is not available on the network\n\
379             3. You need to connect to more peers\n\
380             Try: ipfrs swarm peers (to check connections)"
381        }
382        "permission_denied" => {
383            "Permission denied.\n\
384             Troubleshooting steps:\n\
385             1. Check file/directory permissions\n\
386             2. Ensure you have write access to the data directory\n\
387             3. Try running with appropriate permissions"
388        }
389        "config_error" => {
390            "Configuration error.\n\
391             Troubleshooting steps:\n\
392             1. Check config file syntax (TOML format)\n\
393             2. Verify config file location: ~/.config/ipfrs/config.toml\n\
394             3. Reset to defaults: rm ~/.config/ipfrs/config.toml && ipfrs init"
395        }
396        "network_timeout" => {
397            "Network operation timed out.\n\
398             Troubleshooting steps:\n\
399             1. Check your internet connection\n\
400             2. Try connecting to bootstrap peers\n\
401             3. Increase timeout in config file\n\
402             4. Check if peers are reachable: ipfrs ping <peer-id>"
403        }
404        _ => "For more help, visit: https://github.com/tensorlogic/ipfrs/issues",
405    };
406
407    if is_tty() {
408        println!("\n{} {}", "Hint:".yellow().bold(), hint);
409    } else {
410        println!("\nHint: {}", hint);
411    }
412}
413
414#[cfg(test)]
415mod tests {
416    use super::*;
417
418    #[test]
419    fn test_format_bytes() {
420        assert_eq!(format_bytes(0), "0 B");
421        assert_eq!(format_bytes(512), "512 B");
422        assert_eq!(format_bytes(1024), "1.00 KB");
423        assert_eq!(format_bytes(1536), "1.50 KB");
424        assert_eq!(format_bytes(1048576), "1.00 MB");
425        assert_eq!(format_bytes(1073741824), "1.00 GB");
426    }
427
428    #[test]
429    fn test_format_bytes_detailed() {
430        assert_eq!(format_bytes_detailed(512), "512 bytes");
431        assert_eq!(format_bytes_detailed(1024), "1.00 KB (1024 bytes)");
432    }
433
434    #[test]
435    fn test_table_printer() {
436        let mut table = TablePrinter::new(vec!["Name", "Size", "CID"]);
437        table.add_row(vec!["file.txt", "1024", "Qm..."]);
438        table.add_row(vec!["data.bin", "2048", "Qm..."]);
439        // Just verify it doesn't panic
440        table.print();
441    }
442
443    #[test]
444    fn test_output_style_quiet_mode() {
445        let style = OutputStyle::with_quiet(true, "text", true);
446        assert!(style.is_quiet());
447        assert!(!style.is_compact());
448    }
449
450    #[test]
451    fn test_output_style_quiet_mode_disabled() {
452        let style = OutputStyle::with_quiet(true, "text", false);
453        assert!(!style.is_quiet());
454    }
455
456    #[test]
457    fn test_output_style_quiet_with_json() {
458        let style = OutputStyle::with_quiet(true, "json", true);
459        assert!(style.is_quiet());
460        assert_eq!(style.format, "json");
461    }
462
463    #[test]
464    fn test_output_style_quiet_default() {
465        let style = OutputStyle::default();
466        assert!(!style.is_quiet());
467    }
468}