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 "Processed 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 println!(
51 " Note: '{}' was not deleted — remove it manually if it is no longer needed.",
52 report.input_filename
53 );
54 }
55 Err(err) => {
56 display_error(err);
57 }
58 }
59}
60
61pub fn display_decryption_report(result: WorkflowResult<DecryptionReport>) {
62 match result {
63 Ok(report) => {
64 let msg = format!(
65 "Decrypted '{}' -> '{}' in {:#?} using {}",
66 report.input_filename, report.output_filename, report.duration, report.algorithm
67 );
68 display_success(&msg);
69 }
70 Err(err) => {
71 display_error(err);
72 }
73 }
74}
75
76pub fn display_file_info_list(info_list: &FileInfoList) {
77 if info_list.items.is_empty() {
78 println!("{}", "No shadow files found.".yellow());
79 return;
80 }
81
82 let mut sorted_items = info_list.items.clone();
84 sorted_items.sort_by(|a, b| match (&a.original_filename, &b.original_filename) {
85 (Some(name_a), Some(name_b)) => name_a.as_str().cmp(name_b.as_str()),
86 (Some(_), None) => std::cmp::Ordering::Less,
87 (None, Some(_)) => std::cmp::Ordering::Greater,
88 (None, None) => a.obfuscated_filename.cmp(&b.obfuscated_filename),
89 });
90
91 println!("{}", "Shadow Files Listing".bold().underline());
93 println!();
94
95 println!(
97 "{:<30} {:<30} {:<10} {:<10}",
98 "Original Filename".bold(),
99 "Obfuscated Filename".bold(),
100 "Version".bold(),
101 "Size".bold()
102 );
103 println!("{}", "─".repeat(80).dimmed());
104
105 for info in &sorted_items {
107 let original = match &info.original_filename {
108 Some(name) => name.as_str().green(),
109 None => "N/A".red(),
110 };
111
112 let obfuscated = &info.obfuscated_filename;
113 let version = info.version.as_str().cyan();
114 let size = format_size(info.size).blue();
115
116 println!(
117 "{:<30} {:<30} {:<10} {:<10}",
118 truncate_string(&original, 28),
119 truncate_string(obfuscated, 28),
120 version,
121 size
122 );
123 }
124
125 println!();
126 println!("{} files found", info_list.items.len().to_string().bold());
127}
128
129fn format_size(bytes: u64) -> String {
130 const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
131 let mut size = bytes as f64;
132 let mut unit_index = 0;
133
134 while size >= 1024.0 && unit_index < UNITS.len() - 1 {
135 size /= 1024.0;
136 unit_index += 1;
137 }
138
139 if unit_index == 0 {
140 format!("{} {}", bytes, UNITS[0])
141 } else {
142 format!("{:.1} {}", size, UNITS[unit_index])
143 }
144}
145
146fn truncate_string(s: &str, max_len: usize) -> String {
147 if s.len() <= max_len {
148 s.to_string()
149 } else {
150 format!("{}...", &s[..max_len.saturating_sub(3)])
151 }
152}