1use colored::Colorize;
2use shadow_crypt_core::{
3 progress::ProgressCounter,
4 report::{DecryptionReport, EncryptionReport, KeyDerivationReport},
5};
6
7use crate::{
8 errors::{WorkflowError, WorkflowResult},
9 listing::file::FileInfoList,
10};
11
12pub fn display_progress(counter: &ProgressCounter) {
13 println!(
14 "Processing file {} of {}",
15 counter.get_current(),
16 counter.get_total()
17 );
18}
19
20pub fn display_success(message: &str) {
21 println!("{} {}", "✓".green().bold(), message);
22}
23
24pub fn display_error(error: WorkflowError) {
25 eprintln!("{} {}", "✗".red().bold(), error);
26}
27
28pub fn display_key_derivation_report(report: &KeyDerivationReport) {
29 println!("Derived key in {:#?}:", report.duration);
30 println!(" Algorithm: {}", report.algorithm);
31 println!(" Version: {}", report.algorithm_version);
32 println!(
33 " Memory Cost (KiB): {} ({} MiB)",
34 report.memory_cost_kib,
35 report.memory_cost_kib / 1024
36 );
37 println!(" Time Cost (Iterations): {}", report.time_cost_iterations);
38 println!(" Parallelism: {}", report.parallelism);
39 println!(" Key Size (Bytes): {}", report.key_size_bytes);
40}
41
42pub fn display_encryption_report(result: WorkflowResult<EncryptionReport>) {
43 match result {
44 Ok(report) => {
45 let msg = format!(
46 "Encrypted '{}' -> '{}' in {:#?} using {}",
47 report.input_filename, report.output_filename, report.duration, report.algorithm
48 );
49 display_success(&msg);
50 }
51 Err(err) => {
52 display_error(err);
53 }
54 }
55}
56
57pub fn display_decryption_report(result: WorkflowResult<DecryptionReport>) {
58 match result {
59 Ok(report) => {
60 let msg = format!(
61 "Decrypted '{}' -> '{}' in {:#?} using {}",
62 report.input_filename, report.output_filename, report.duration, report.algorithm
63 );
64 display_success(&msg);
65 }
66 Err(err) => {
67 display_error(err);
68 }
69 }
70}
71
72pub fn display_file_info_list(info_list: &FileInfoList) {
73 if info_list.items.is_empty() {
74 println!("{}", "No shadow files found.".yellow());
75 return;
76 }
77
78 let mut sorted_items = info_list.items.clone();
80 sorted_items.sort_by(|a, b| match (&a.original_filename, &b.original_filename) {
81 (Some(name_a), Some(name_b)) => name_a.as_str().cmp(name_b.as_str()),
82 (Some(_), None) => std::cmp::Ordering::Less,
83 (None, Some(_)) => std::cmp::Ordering::Greater,
84 (None, None) => a.obfuscated_filename.cmp(&b.obfuscated_filename),
85 });
86
87 println!("{}", "Shadow Files Listing".bold().underline());
89 println!();
90
91 println!(
93 "{:<30} {:<30} {:<10} {:<10}",
94 "Original Filename".bold(),
95 "Obfuscated Filename".bold(),
96 "Version".bold(),
97 "Size".bold()
98 );
99 println!("{}", "─".repeat(80).dimmed());
100
101 for info in &sorted_items {
103 let original = match &info.original_filename {
104 Some(name) => name.as_str().green(),
105 None => "N/A".red(),
106 };
107
108 let obfuscated = &info.obfuscated_filename;
109 let version = info.version.as_str().cyan();
110 let size = format_size(info.size).blue();
111
112 println!(
113 "{:<30} {:<30} {:<10} {:<10}",
114 truncate_string(&original, 28),
115 truncate_string(obfuscated, 28),
116 version,
117 size
118 );
119 }
120
121 println!();
122 println!("{} files found", info_list.items.len().to_string().bold());
123}
124
125fn format_size(bytes: u64) -> String {
126 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
127 let mut size = bytes as f64;
128 let mut unit_index = 0;
129
130 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
131 size /= 1024.0;
132 unit_index += 1;
133 }
134
135 if unit_index == 0 {
136 format!("{} {}", bytes, UNITS[0])
137 } else {
138 format!("{:.1} {}", size, UNITS[unit_index])
139 }
140}
141
142fn truncate_string(s: &str, max_len: usize) -> String {
143 if s.len() <= max_len {
144 s.to_string()
145 } else {
146 format!("{}...", &s[..max_len.saturating_sub(3)])
147 }
148}