theater_cli/output/
mod.rs1pub 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#[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 pub fn success(&self, message: &str) -> CliResult<()> {
40 println!("{} {}", self.theme.success_icon(), message);
41 Ok(())
42 }
43
44 pub fn error(&self, message: &str) -> CliResult<()> {
46 eprintln!("{} {}", self.theme.error_icon(), message);
47 Ok(())
48 }
49
50 pub fn warning(&self, message: &str) -> CliResult<()> {
52 println!("{} {}", self.theme.warning_icon(), message);
53 Ok(())
54 }
55
56 pub fn info(&self, message: &str) -> CliResult<()> {
58 println!("{} {}", self.theme.info_icon(), message);
59 Ok(())
60 }
61
62 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(crate::error::CliError::Serialization)?;
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 pub fn term(&self) -> &Term {
106 &self.term
107 }
108
109 pub fn theme(&self) -> &Theme {
111 &self.theme
112 }
113
114 pub fn config(&self) -> &OutputConfig {
116 &self.config
117 }
118
119 pub fn progress_bar(&self, len: u64) -> ProgressBar {
121 ProgressBar::new(len, self.theme.clone())
122 }
123
124 pub fn table(&self, headers: &[&str], rows: &[Vec<String>]) -> CliResult<()> {
126 if rows.is_empty() {
127 return Ok(());
128 }
129
130 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 if let Some(max_width) = self.config.max_width {
143 let available_width = max_width.saturating_sub(widths.len() * 3); let total_width: usize = widths.iter().sum();
145
146 if total_width > available_width {
147 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!("│");
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!("├");
168 for width in &widths {
169 print!("{}", "─".repeat(width + 2));
170 print!("┼");
171 }
172 println!();
173
174 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
193pub 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}