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        "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    // Sort the items: files with original names first (alphabetically), then files without
79    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    // Print header
88    println!("{}", "Shadow Files Listing".bold().underline());
89    println!();
90
91    // Column headers
92    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    // Print each file info
102    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}