Skip to main content

shadow_crypt_shell/
ui.rs

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    // Sort the items: files with original names first (alphabetically), then files without
83    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    // Print header
92    println!("{}", "Shadow Files Listing".bold().underline());
93    println!();
94
95    // Column headers
96    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    // Print each file info
106    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}