Skip to main content

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