theater_cli/output/
mod.rs

1pub mod formatters;
2mod progress;
3mod theme;
4
5pub use formatters::*;
6pub use progress::*;
7pub use theme::*;
8
9use console::Term;
10
11use crate::config::OutputConfig;
12use crate::error::CliResult;
13
14/// Main output handler for the CLI
15#[derive(Debug)]
16pub struct OutputManager {
17    config: OutputConfig,
18    term: Term,
19    theme: Theme,
20}
21
22impl OutputManager {
23    pub fn new(config: OutputConfig) -> Self {
24        let term = Term::stdout();
25        let theme = if config.colors && term.features().colors_supported() {
26            Theme::colored()
27        } else {
28            Theme::plain()
29        };
30
31        Self {
32            config,
33            term,
34            theme,
35        }
36    }
37
38    /// Print a success message
39    pub fn success(&self, message: &str) -> CliResult<()> {
40        println!("{} {}", self.theme.success_icon(), message);
41        Ok(())
42    }
43
44    /// Print an error message
45    pub fn error(&self, message: &str) -> CliResult<()> {
46        eprintln!("{} {}", self.theme.error_icon(), message);
47        Ok(())
48    }
49
50    /// Print a warning message
51    pub fn warning(&self, message: &str) -> CliResult<()> {
52        println!("{} {}", self.theme.warning_icon(), message);
53        Ok(())
54    }
55
56    /// Print an info message
57    pub fn info(&self, message: &str) -> CliResult<()> {
58        println!("{} {}", self.theme.info_icon(), message);
59        Ok(())
60    }
61
62    /// Print formatted output based on the configured format
63    pub fn output<T>(&self, data: &T, format: Option<&str>) -> CliResult<()>
64    where
65        T: serde::Serialize + OutputFormat,
66    {
67        let format = format.unwrap_or(&self.config.default_format);
68
69        match format {
70            "json" => {
71                let json = serde_json::to_string_pretty(data)
72                    .map_err(|e| crate::error::CliError::Serialization(e))?;
73                println!("{}", json);
74            }
75            "yaml" => {
76                let yaml = serde_yaml::to_string(data)
77                    .map_err(|e| crate::error::CliError::Internal(e.into()))?;
78                println!("{}", yaml);
79            }
80            "compact" => {
81                data.format_compact(self)?;
82            }
83            "pretty" => {
84                data.format_pretty(self)?;
85            }
86            "table" => {
87                data.format_table(self)?;
88            }
89            "detailed" => {
90                data.format_detailed(self)?;
91            }
92            _ => {
93                return Err(crate::error::CliError::invalid_input(
94                    "format",
95                    format,
96                    "Supported formats: json, yaml, compact, pretty, table",
97                ));
98            }
99        }
100
101        Ok(())
102    }
103
104    /// Get the terminal
105    pub fn term(&self) -> &Term {
106        &self.term
107    }
108
109    /// Get the theme
110    pub fn theme(&self) -> &Theme {
111        &self.theme
112    }
113
114    /// Get the output configuration
115    pub fn config(&self) -> &OutputConfig {
116        &self.config
117    }
118
119    /// Create a progress bar
120    pub fn progress_bar(&self, len: u64) -> ProgressBar {
121        ProgressBar::new(len, self.theme.clone())
122    }
123
124    /// Print a table with headers and rows
125    pub fn table(&self, headers: &[&str], rows: &[Vec<String>]) -> CliResult<()> {
126        if rows.is_empty() {
127            return Ok(());
128        }
129
130        // Calculate column widths
131        let mut widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
132
133        for row in rows {
134            for (i, cell) in row.iter().enumerate() {
135                if i < widths.len() {
136                    widths[i] = widths[i].max(cell.len());
137                }
138            }
139        }
140
141        // Apply max width if configured
142        if let Some(max_width) = self.config.max_width {
143            let available_width = max_width.saturating_sub(widths.len() * 3); // Account for separators
144            let total_width: usize = widths.iter().sum();
145
146            if total_width > available_width {
147                // Proportionally reduce column widths
148                let scale = available_width as f64 / total_width as f64;
149                for width in &mut widths {
150                    *width = (*width as f64 * scale) as usize;
151                }
152            }
153        }
154
155        // Print header
156        print!("│");
157        for (i, header) in headers.iter().enumerate() {
158            print!(
159                " {:width$} │",
160                self.theme.table_header().apply_to(header),
161                width = widths[i]
162            );
163        }
164        println!();
165
166        // Print separator
167        print!("├");
168        for width in &widths {
169            print!("{}", "─".repeat(width + 2));
170            print!("┼");
171        }
172        println!();
173
174        // Print rows
175        for row in rows {
176            print!("│");
177            for (i, cell) in row.iter().enumerate() {
178                let truncated = if i < widths.len() && cell.len() > widths[i] {
179                    format!("{}…", &cell[..widths[i].saturating_sub(1)])
180                } else {
181                    cell.clone()
182                };
183
184                print!(" {:width$} │", truncated, width = widths[i]);
185            }
186            println!();
187        }
188
189        Ok(())
190    }
191}
192
193/// Trait for types that can be formatted in different ways
194pub trait OutputFormat {
195    fn format_compact(&self, output: &OutputManager) -> CliResult<()>;
196    fn format_pretty(&self, output: &OutputManager) -> CliResult<()>;
197    fn format_table(&self, output: &OutputManager) -> CliResult<()>;
198    fn format_detailed(&self, output: &OutputManager) -> CliResult<()>;
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use crate::config::OutputConfig;
205
206    #[test]
207    fn test_output_manager_creation() {
208        let config = OutputConfig {
209            default_format: "json".to_string(),
210            colors: true,
211            timestamps: true,
212            max_width: Some(100),
213        };
214
215        let output = OutputManager::new(config);
216        assert_eq!(output.config().default_format, "json");
217    }
218}