subx_cli/cli/
ui.rs

1//! User interface utilities and display helpers for SubX CLI.
2//!
3//! This module provides a comprehensive set of utilities for creating
4//! consistent and user-friendly command-line interfaces. It handles
5//! status messages, progress indicators, result displays, and AI usage
6//! statistics with consistent styling and formatting.
7//!
8//! # Features
9//!
10//! - **Status Messages**: Success, error, and warning message formatting
11//! - **Progress Indicators**: Configurable progress bars for long operations
12//! - **Result Display**: Formatted tables and structured output
13//! - **AI Statistics**: Usage tracking and cost information display
14//! - **Consistent Styling**: Color-coded messages with Unicode symbols
15//!
16//! # Design Principles
17//!
18//! - **Accessibility**: Clear visual hierarchy with color and symbols
19//! - **Configurability**: Respects user preferences for progress display
20//! - **Consistency**: Unified styling across all CLI operations
21//! - **Informativeness**: Rich context and actionable information
22//!
23//! # Examples
24//!
25//! ```rust
26//! use subx_cli::cli::ui;
27//!
28//! // Display status messages
29//! ui::print_success("Subtitle files processed successfully");
30//! ui::print_warning("File format might be incompatible");
31//! ui::print_error("Unable to read configuration file");
32//!
33//! // Create progress bar for batch operations
34//! let progress = ui::create_progress_bar(100);
35//! for i in 0..100 {
36//!     progress.inc(1);
37//!     // ... processing ...
38//! }
39//! progress.finish_with_message("Processing completed");
40//! ```
41
42// src/cli/ui.rs
43use crate::cli::table::{MatchDisplayRow, create_match_table};
44use crate::core::matcher::MatchOperation;
45use colored::*;
46use indicatif::{ProgressBar, ProgressStyle};
47
48/// Display a success message with consistent formatting.
49///
50/// Prints a success message with a green checkmark symbol and styled text.
51/// Used throughout the application to indicate successful completion of
52/// operations such as file processing, configuration updates, or command execution.
53///
54/// # Format
55/// ```text
56/// ✓ [message]
57/// ```
58///
59/// # Examples
60///
61/// ```rust
62/// use subx_cli::cli::ui::print_success;
63///
64/// print_success("Successfully processed 15 subtitle files");
65/// print_success("Configuration saved to ~/.config/subx/config.toml");
66/// print_success("AI matching completed with 98% confidence");
67/// ```
68///
69/// # Output Examples
70/// ```text
71/// ✓ Successfully processed 15 subtitle files
72/// ✓ Configuration saved to ~/.config/subx/config.toml
73/// ✓ AI matching completed with 98% confidence
74/// ```
75pub fn print_success(message: &str) {
76    println!("{} {}", "✓".green().bold(), message);
77}
78
79/// Display an error message with consistent formatting.
80///
81/// Prints an error message to stderr with a red X symbol and styled text.
82/// Used for reporting errors, failures, and critical issues that prevent
83/// operation completion. Messages are sent to stderr to separate them
84/// from normal program output.
85///
86/// # Format
87/// ```text
88/// ✗ [message]
89/// ```
90///
91/// # Examples
92///
93/// ```rust
94/// use subx_cli::cli::ui::print_error;
95///
96/// print_error("Failed to load configuration file");
97/// print_error("AI API request timed out after 30 seconds");
98/// print_error("Invalid subtitle format detected");
99/// ```
100///
101/// # Output Examples
102/// ```text
103/// ✗ Failed to load configuration file
104/// ✗ AI API request timed out after 30 seconds
105/// ✗ Invalid subtitle format detected
106/// ```
107pub fn print_error(message: &str) {
108    eprintln!("{} {}", "✗".red().bold(), message);
109}
110
111/// Display a warning message with consistent formatting.
112///
113/// Prints a warning message with a yellow warning symbol and styled text.
114/// Used for non-critical issues, deprecation notices, or situations that
115/// may require user attention but don't prevent operation completion.
116///
117/// # Format
118/// ```text
119/// ⚠ [message]
120/// ```
121///
122/// # Examples
123///
124/// ```rust
125/// use subx_cli::cli::ui::print_warning;
126///
127/// print_warning("Legacy subtitle format detected, consider upgrading");
128/// print_warning("AI confidence below 80%, manual review recommended");
129/// print_warning("Configuration file not found, using defaults");
130/// ```
131///
132/// # Output Examples
133/// ```text
134/// ⚠ Legacy subtitle format detected, consider upgrading
135/// ⚠ AI confidence below 80%, manual review recommended
136/// ⚠ Configuration file not found, using defaults
137/// ```
138pub fn print_warning(message: &str) {
139    println!("{} {}", "⚠".yellow().bold(), message);
140}
141
142/// Create a progress bar with consistent styling and configuration.
143///
144/// Creates a progress bar with customized styling that respects user
145/// configuration preferences. The progress bar can be hidden based on
146/// the `enable_progress_bar` configuration setting, allowing users to
147/// disable progress indicators if desired.
148///
149/// # Configuration Integration
150///
151/// The progress bar visibility is controlled by the configuration setting:
152/// ```toml
153/// [general]
154/// enable_progress_bar = true  # Show progress bars
155/// # or
156/// enable_progress_bar = false # Hide progress bars
157/// ```
158///
159/// # Progress Bar Features
160///
161/// - **Animated spinner**: Indicates ongoing activity
162/// - **Elapsed time**: Shows time since operation started
163/// - **Progress bar**: Visual representation of completion percentage
164/// - **ETA estimation**: Estimated time to completion
165/// - **Current/total counts**: Numeric progress indicator
166///
167/// # Template Format
168/// ```text
169/// ⠋ [00:01:23] [████████████████████████████████████████] 75/100 (00:00:17)
170/// ```
171///
172/// # Arguments
173///
174/// * `total` - The total number of items to be processed
175///
176/// # Returns
177///
178/// A configured `ProgressBar` instance ready for use
179///
180/// # Examples
181///
182/// ```rust
183/// use subx_cli::cli::ui::create_progress_bar;
184///
185/// // Create progress bar for 100 items
186/// let progress = create_progress_bar(100);
187///
188/// for i in 0..100 {
189///     // ... process item ...
190///     progress.inc(1);
191///     
192///     if i % 10 == 0 {
193///         progress.set_message(format!("Processing item {}", i));
194///     }
195/// }
196///
197/// progress.finish_with_message("✓ All items processed successfully");
198/// ```
199///
200/// # Error Handling
201///
202/// If configuration loading fails, the progress bar will default to visible.
203/// This ensures that progress indication is available even when configuration
204/// is problematic.
205pub fn create_progress_bar(total: u64) -> ProgressBar {
206    let pb = ProgressBar::new(total);
207    // Progress bar is visible by default
208    // Configuration-based control should be handled by the caller
209    pb.set_style(
210        ProgressStyle::default_bar()
211            .template(
212                "{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} ({eta})",
213            )
214            .unwrap(),
215    );
216    pb
217}
218
219/// Display comprehensive AI API usage statistics and cost information.
220///
221/// Presents detailed information about AI API calls including token usage,
222/// model information, and cost implications. This helps users understand
223/// their AI service consumption and manage usage costs effectively.
224///
225/// # Information Displayed
226///
227/// - **Model Name**: The specific AI model used for processing
228/// - **Token Breakdown**: Detailed token usage by category
229///   - Prompt tokens: Input text sent to the AI
230///   - Completion tokens: AI-generated response text
231///   - Total tokens: Sum of prompt and completion tokens
232/// - **Cost Implications**: Helps users understand billing impact
233///
234/// # Format Example
235/// ```text
236/// 🤖 AI API Call Details:
237///    Model: gpt-4-turbo-preview
238///    Prompt tokens: 1,247
239///    Completion tokens: 892
240///    Total tokens: 2,139
241/// ```
242///
243/// # Arguments
244///
245/// * `usage` - AI usage statistics containing token counts and model information
246///
247/// # Examples
248///
249/// ```rust
250/// use subx_cli::cli::ui::display_ai_usage;
251/// use subx_cli::services::ai::AiUsageStats;
252///
253/// let usage = AiUsageStats {
254///     model: "gpt-4-turbo-preview".to_string(),
255///     prompt_tokens: 1247,
256///     completion_tokens: 892,
257///     total_tokens: 2139,
258/// };
259///
260/// display_ai_usage(&usage);
261/// ```
262///
263/// # Use Cases
264///
265/// - **Cost monitoring**: Track API usage for billing awareness
266/// - **Performance analysis**: Understand token efficiency
267/// - **Debugging**: Verify expected model usage
268/// - **Optimization**: Identify opportunities to reduce token consumption
269pub fn display_ai_usage(usage: &crate::services::ai::AiUsageStats) {
270    println!("🤖 AI API Call Details:");
271    println!("   Model: {}", usage.model);
272    println!("   Prompt tokens: {}", usage.prompt_tokens);
273    println!("   Completion tokens: {}", usage.completion_tokens);
274    println!("   Total tokens: {}", usage.total_tokens);
275    println!();
276}
277
278/// Display file matching results with support for dry-run preview mode.
279pub fn display_match_results(results: &[MatchOperation], is_dry_run: bool) {
280    if results.is_empty() {
281        println!("{}", "No matching file pairs found".yellow());
282        return;
283    }
284
285    println!("\n{}", "📋 File Matching Results".bold().blue());
286    if is_dry_run {
287        println!(
288            "{}",
289            "🔍 Preview mode (files will not be modified)".yellow()
290        );
291    }
292    println!();
293
294    // Split each match result into multiple lines: video, subtitle, new name, and optionally relocation
295    let rows: Vec<MatchDisplayRow> = results
296        .iter()
297        .enumerate()
298        .flat_map(|(i, op)| {
299            let idx = i + 1;
300            let video = op.video_file.path.to_string_lossy();
301            let subtitle = op.subtitle_file.path.to_string_lossy();
302            let new_name = &op.new_subtitle_name;
303
304            // Add status symbol and tree structure
305            let status_symbol = if is_dry_run { "🔍" } else { "✓" };
306
307            let mut rows = vec![
308                MatchDisplayRow {
309                    file_type: format!("{status_symbol} Video {idx}"),
310                    file_path: video.to_string(),
311                },
312                MatchDisplayRow {
313                    file_type: format!("├ Subtitle {idx}"),
314                    file_path: subtitle.to_string(),
315                },
316                MatchDisplayRow {
317                    file_type: format!("├ New name {idx}"),
318                    file_path: new_name.clone(),
319                },
320            ];
321
322            // Add relocation operation row if needed
323            if op.requires_relocation {
324                let operation_icon = match op.relocation_mode {
325                    crate::core::matcher::engine::FileRelocationMode::Copy => "📄",
326                    crate::core::matcher::engine::FileRelocationMode::Move => "📁",
327                    _ => "",
328                };
329
330                let operation_verb = match op.relocation_mode {
331                    crate::core::matcher::engine::FileRelocationMode::Copy => "Copy to",
332                    crate::core::matcher::engine::FileRelocationMode::Move => "Move to",
333                    _ => "",
334                };
335
336                if let Some(target_path) = &op.relocation_target_path {
337                    rows.push(MatchDisplayRow {
338                        file_type: format!("└ {operation_icon} {operation_verb}"),
339                        file_path: target_path.to_string_lossy().to_string(),
340                    });
341                } else {
342                    // Update the last row to have the proper tree ending
343                    if let Some(last_row) = rows.last_mut() {
344                        last_row.file_type = last_row.file_type.replace("├", "└");
345                    }
346                }
347            } else {
348                // Update the last row to have the proper tree ending
349                if let Some(last_row) = rows.last_mut() {
350                    last_row.file_type = last_row.file_type.replace("├", "└");
351                }
352            }
353
354            rows
355        })
356        .collect();
357
358    println!("{}", create_match_table(rows));
359
360    println!(
361        "\n{}",
362        format!("Total processed {} file mappings", results.len()).bold()
363    );
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369
370    #[test]
371    fn test_match_table_display() {
372        let rows = vec![
373            MatchDisplayRow {
374                file_type: "✓ Video 1".to_string(),
375                file_path: "movie1.mp4".to_string(),
376            },
377            MatchDisplayRow {
378                file_type: "├ Subtitle 1".to_string(),
379                file_path: "subtitle1.srt".to_string(),
380            },
381            MatchDisplayRow {
382                file_type: "└ New name 1".to_string(),
383                file_path: "movie1.srt".to_string(),
384            },
385        ];
386        let table = create_match_table(rows);
387        assert!(table.contains("✓ Video 1"));
388        assert!(table.contains("movie1.mp4"));
389        assert!(table.contains("├ Subtitle 1"));
390        assert!(table.contains("subtitle1.srt"));
391        assert!(table.contains("└ New name 1"));
392        assert!(table.contains("movie1.srt"));
393    }
394}