forge/output/
formatter.rs

1use crate::cli::Args;
2use crate::openssl::ParsedPfx;
3use crate::output::OutputConfig;
4use colored::*;
5use console::Term;
6use std::io::{self, Write};
7use tabled::{
8    Table, Tabled,
9    settings::{Alignment, Modify, Style, object::Rows},
10};
11
12/// Handles detailed TUI output formatting
13pub struct OutputFormatter {
14    config: OutputConfig,
15}
16
17#[derive(Tabled)]
18struct FileOutput {
19    #[tabled(rename = "Type")]
20    file_type: String,
21    #[tabled(rename = "Filename")]
22    filename: String,
23    #[tabled(rename = "Location")]
24    location: String,
25    #[tabled(rename = "Status")]
26    status: String,
27}
28
29#[derive(Tabled)]
30struct CertInfo {
31    #[tabled(rename = "Property")]
32    property: String,
33    #[tabled(rename = "Value")]
34    value: String,
35}
36
37impl OutputFormatter {
38    pub fn new(config: &OutputConfig) -> Self {
39        Self {
40            config: config.clone(),
41        }
42    }
43
44    /// Print a beautifully formatted summary
45    pub fn print_summary(
46        &self,
47        args: &Args,
48        parsed: &ParsedPfx,
49        term: &mut Term,
50    ) -> io::Result<()> {
51        // Header
52        self.print_header("Conversion Summary", term)?;
53
54        // Create table data
55        let mut files = vec![
56            FileOutput {
57                file_type: "Private Key".to_string(),
58                filename: args.key_filename().to_string(),
59                location: args.output_dir().to_string(),
60                status: if self.config.use_colors {
61                    "✓ Created".green().to_string()
62                } else {
63                    "✓ Created".to_string()
64                },
65            },
66            FileOutput {
67                file_type: "Certificate".to_string(),
68                filename: args.cert_filename().to_string(),
69                location: args.output_dir().to_string(),
70                status: if self.config.use_colors {
71                    "✓ Created".green().to_string()
72                } else {
73                    "✓ Created".to_string()
74                },
75            },
76        ];
77
78        // Add combined file if requested
79        if args.combined {
80            files.push(FileOutput {
81                file_type: "Combined PEM".to_string(),
82                filename: args.combined_filename().to_string(),
83                location: args.output_dir().to_string(),
84                status: if self.config.use_colors {
85                    "✓ Created".green().to_string()
86                } else {
87                    "✓ Created".to_string()
88                },
89            });
90        }
91
92        // Add chain files if requested
93        if args.chain && parsed.has_chain() {
94            files.push(FileOutput {
95                file_type: "Certificate Chain".to_string(),
96                filename: "certificate_chain.pem".to_string(),
97                location: args.output_dir().to_string(),
98                status: if self.config.use_colors {
99                    "✓ Created".green().to_string()
100                } else {
101                    "✓ Created".to_string()
102                },
103            });
104
105            for i in 0..parsed.chain_length() {
106                files.push(FileOutput {
107                    file_type: format!("Chain Cert {}", i + 1),
108                    filename: format!("chain_cert_{}.pem", i + 1),
109                    location: args.output_dir().to_string(),
110                    status: if self.config.use_colors {
111                        "✓ Created".green().to_string()
112                    } else {
113                        "✓ Created".to_string()
114                    },
115                });
116            }
117        }
118
119        // Create and style the table
120        let mut table = Table::new(&files);
121        table
122            .with(Style::rounded())
123            .with(Modify::new(Rows::first()).with(Alignment::center()));
124
125        if self.config.use_colors {
126            writeln!(term, "{}", table.to_string().bright_white())?;
127        } else {
128            writeln!(term, "{}", table)?;
129        }
130
131        // Statistics box
132        self.print_stats_box(parsed, term)?;
133
134        // Footer
135        if self.config.use_colors {
136            writeln!(
137                term,
138                "\n{} Conversion completed successfully!",
139                "🎉".bright_green()
140            )?;
141        } else {
142            writeln!(term, "\n✓ Conversion completed successfully!")?;
143        }
144
145        Ok(())
146    }
147
148    /// Print certificate information in a formatted way
149    pub fn print_cert_info(&self, parsed: &ParsedPfx, term: &mut Term) -> io::Result<()> {
150        self.print_header("Certificate Information", term)?;
151
152        let cert_info = parsed.certificate_info();
153
154        let cert_data = vec![
155            CertInfo {
156                property: "Subject".to_string(),
157                value: cert_info.subject,
158            },
159            CertInfo {
160                property: "Issuer".to_string(),
161                value: cert_info.issuer,
162            },
163            CertInfo {
164                property: "Serial Number".to_string(),
165                value: cert_info.serial_number,
166            },
167            CertInfo {
168                property: "Valid From".to_string(),
169                value: cert_info.not_before,
170            },
171            CertInfo {
172                property: "Valid Until".to_string(),
173                value: cert_info.not_after,
174            },
175            CertInfo {
176                property: "Signature Algorithm".to_string(),
177                value: cert_info.signature_algorithm,
178            },
179        ];
180
181        let mut table = Table::new(&cert_data);
182        table
183            .with(Style::rounded())
184            .with(Modify::new(Rows::first()).with(Alignment::center()));
185
186        if self.config.use_colors {
187            writeln!(term, "{}", table.to_string().bright_white())?;
188        } else {
189            writeln!(term, "{}", table)?;
190        }
191
192        Ok(())
193    }
194
195    /// Print a stylized header
196    fn print_header(&self, title: &str, term: &mut Term) -> io::Result<()> {
197        let width = 60;
198        let padding = (width - title.len() - 2) / 2;
199
200        if self.config.use_colors {
201            writeln!(term, "\n{}", "═".repeat(width).bright_cyan())?;
202            writeln!(
203                term,
204                "{}{}{}{}",
205                "║".bright_cyan(),
206                " ".repeat(padding),
207                title.bold().bright_white(),
208                " ".repeat(width - padding - title.len() - 2) + "║"
209            )?;
210            writeln!(term, "{}", "═".repeat(width).bright_cyan())?;
211        } else {
212            writeln!(term, "\n{}", "=".repeat(width))?;
213            writeln!(
214                term,
215                "|{}{}{}|",
216                " ".repeat(padding),
217                title,
218                " ".repeat(width - padding - title.len() - 2)
219            )?;
220            writeln!(term, "{}", "=".repeat(width))?;
221        }
222
223        Ok(())
224    }
225
226    /// Print statistics in a box
227    fn print_stats_box(&self, parsed: &ParsedPfx, term: &mut Term) -> io::Result<()> {
228        writeln!(term)?;
229
230        if self.config.use_colors {
231            writeln!(
232                term,
233                "{}",
234                "┌─ Statistics ─────────────────────────────────┐".bright_blue()
235            )?;
236            writeln!(
237                term,
238                "{} Files generated: {}                        {}",
239                "│".bright_blue(),
240                if parsed.has_chain() { "5+" } else { "2-3" }.bright_yellow(),
241                "│".bright_blue()
242            )?;
243            writeln!(
244                term,
245                "{} Certificate chain: {}                      {}",
246                "│".bright_blue(),
247                if parsed.has_chain() {
248                    format!("{} certificates", parsed.chain_length() + 1).bright_green()
249                } else {
250                    "No chain".bright_red()
251                },
252                "│".bright_blue()
253            )?;
254            writeln!(
255                term,
256                "{} Private key format: {}                     {}",
257                "│".bright_blue(),
258                "PKCS#8 PEM".bright_green(),
259                "│".bright_blue()
260            )?;
261            writeln!(
262                term,
263                "{}",
264                "└─────────────────────────────────────────────┘".bright_blue()
265            )?;
266        } else {
267            writeln!(term, "┌─ Statistics ─────────────────────────────────┐")?;
268            writeln!(
269                term,
270                "│ Files generated: {}                        │",
271                if parsed.has_chain() { "5+" } else { "2-3" }
272            )?;
273            writeln!(
274                term,
275                "│ Certificate chain: {}                      │",
276                if parsed.has_chain() {
277                    format!("{} certificates", parsed.chain_length() + 1)
278                } else {
279                    "No chain".to_string()
280                }
281            )?;
282            writeln!(term, "│ Private key format: PKCS#8 PEM             │")?;
283            writeln!(term, "└─────────────────────────────────────────────┘")?;
284        }
285
286        Ok(())
287    }
288}