Skip to main content

voirs_cli/commands/
history.rs

1//! Command history tracking and suggestions.
2//!
3//! This module tracks command usage and provides intelligent suggestions
4//! based on historical patterns.
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::fs;
10use std::path::PathBuf;
11use voirs_sdk::Result;
12
13/// History entry for a command execution
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct HistoryEntry {
16    /// Command name
17    pub command: String,
18    /// Command arguments (sanitized - no sensitive data)
19    pub args: Vec<String>,
20    /// Timestamp of execution
21    pub timestamp: DateTime<Utc>,
22    /// Execution status (success/failure)
23    pub status: ExecutionStatus,
24    /// Duration in milliseconds
25    pub duration_ms: Option<u64>,
26}
27
28/// Execution status
29#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub enum ExecutionStatus {
31    /// Command succeeded
32    Success,
33    /// Command failed
34    Failed,
35    /// Command was cancelled
36    Cancelled,
37}
38
39/// Command history manager
40pub struct HistoryManager {
41    /// Path to history file
42    history_path: PathBuf,
43    /// Maximum entries to keep
44    max_entries: usize,
45}
46
47impl HistoryManager {
48    /// Create a new history manager
49    pub fn new() -> Result<Self> {
50        let history_path = Self::get_history_path()?;
51        Ok(Self {
52            history_path,
53            max_entries: 1000,
54        })
55    }
56
57    /// Get the path to the history file
58    fn get_history_path() -> Result<PathBuf> {
59        let data_dir = dirs::data_dir()
60            .ok_or_else(|| voirs_sdk::VoirsError::config_error("Could not find data directory"))?;
61
62        let voirs_dir = data_dir.join("voirs");
63        fs::create_dir_all(&voirs_dir).map_err(|e| voirs_sdk::VoirsError::IoError {
64            path: voirs_dir.clone(),
65            operation: voirs_sdk::error::IoOperation::Write,
66            source: e,
67        })?;
68
69        Ok(voirs_dir.join("history.json"))
70    }
71
72    /// Load history from file
73    pub fn load(&self) -> Result<Vec<HistoryEntry>> {
74        if !self.history_path.exists() {
75            return Ok(Vec::new());
76        }
77
78        let content =
79            fs::read_to_string(&self.history_path).map_err(|e| voirs_sdk::VoirsError::IoError {
80                path: self.history_path.clone(),
81                operation: voirs_sdk::error::IoOperation::Read,
82                source: e,
83            })?;
84
85        let entries: Vec<HistoryEntry> = serde_json::from_str(&content).unwrap_or_default();
86        Ok(entries)
87    }
88
89    /// Save history to file
90    fn save(&self, entries: &[HistoryEntry]) -> Result<()> {
91        let content = serde_json::to_string_pretty(entries).map_err(|e| {
92            voirs_sdk::VoirsError::config_error(format!("Failed to serialize history: {}", e))
93        })?;
94
95        fs::write(&self.history_path, content).map_err(|e| voirs_sdk::VoirsError::IoError {
96            path: self.history_path.clone(),
97            operation: voirs_sdk::error::IoOperation::Write,
98            source: e,
99        })?;
100
101        Ok(())
102    }
103
104    /// Add a command to history
105    pub fn add_entry(&self, entry: HistoryEntry) -> Result<()> {
106        let mut entries = self.load()?;
107        entries.push(entry);
108
109        // Trim to max entries
110        if entries.len() > self.max_entries {
111            entries.drain(0..(entries.len() - self.max_entries));
112        }
113
114        self.save(&entries)
115    }
116
117    /// Get recent command history
118    pub fn get_recent(&self, limit: usize) -> Result<Vec<HistoryEntry>> {
119        let entries = self.load()?;
120        let start = if entries.len() > limit {
121            entries.len() - limit
122        } else {
123            0
124        };
125        Ok(entries[start..].to_vec())
126    }
127
128    /// Get command usage statistics
129    pub fn get_statistics(&self) -> Result<CommandStatistics> {
130        let entries = self.load()?;
131        let mut stats = CommandStatistics::default();
132
133        let mut command_counts: HashMap<String, usize> = HashMap::new();
134        let mut total_success = 0;
135        let mut total_failed = 0;
136
137        for entry in &entries {
138            *command_counts.entry(entry.command.clone()).or_insert(0) += 1;
139
140            match entry.status {
141                ExecutionStatus::Success => total_success += 1,
142                ExecutionStatus::Failed => total_failed += 1,
143                ExecutionStatus::Cancelled => {}
144            }
145        }
146
147        stats.total_commands = entries.len();
148        stats.total_success = total_success;
149        stats.total_failed = total_failed;
150        stats.command_counts = command_counts;
151
152        // Find most used command
153        if let Some((cmd, count)) = stats.command_counts.iter().max_by_key(|(_, count)| *count) {
154            stats.most_used_command = Some(cmd.clone());
155            stats.most_used_count = *count;
156        }
157
158        Ok(stats)
159    }
160
161    /// Get intelligent suggestions based on history
162    pub fn get_suggestions(&self, limit: usize) -> Result<Vec<String>> {
163        let entries = self.load()?;
164        let mut suggestions = Vec::new();
165
166        // Get recent successful commands
167        let recent_successful: Vec<_> = entries
168            .iter()
169            .rev()
170            .filter(|e| e.status == ExecutionStatus::Success)
171            .take(limit * 2)
172            .collect();
173
174        // Build frequency map
175        let mut freq_map: HashMap<String, usize> = HashMap::new();
176        for entry in recent_successful {
177            let cmd_line = format!("{} {}", entry.command, entry.args.join(" "));
178            *freq_map.entry(cmd_line).or_insert(0) += 1;
179        }
180
181        // Sort by frequency and take top suggestions
182        let mut freq_vec: Vec<_> = freq_map.into_iter().collect();
183        freq_vec.sort_by(|a, b| b.1.cmp(&a.1));
184
185        for (cmd, _) in freq_vec.iter().take(limit) {
186            suggestions.push(cmd.clone());
187        }
188
189        Ok(suggestions)
190    }
191
192    /// Clear all history
193    pub fn clear(&self) -> Result<()> {
194        if self.history_path.exists() {
195            fs::remove_file(&self.history_path).map_err(|e| voirs_sdk::VoirsError::IoError {
196                path: self.history_path.clone(),
197                operation: voirs_sdk::error::IoOperation::Delete,
198                source: e,
199            })?;
200        }
201        Ok(())
202    }
203}
204
205impl Default for HistoryManager {
206    fn default() -> Self {
207        Self::new().expect("Failed to create history manager")
208    }
209}
210
211/// Command usage statistics
212#[derive(Debug, Default)]
213pub struct CommandStatistics {
214    /// Total number of commands executed
215    pub total_commands: usize,
216    /// Number of successful executions
217    pub total_success: usize,
218    /// Number of failed executions
219    pub total_failed: usize,
220    /// Command usage counts
221    pub command_counts: HashMap<String, usize>,
222    /// Most used command
223    pub most_used_command: Option<String>,
224    /// Count of most used command
225    pub most_used_count: usize,
226}
227
228/// Run history command
229pub async fn run_history(limit: usize, show_stats: bool, suggest: bool, clear: bool) -> Result<()> {
230    let manager = HistoryManager::new()?;
231
232    if clear {
233        manager.clear()?;
234        println!("✅ Command history cleared");
235        return Ok(());
236    }
237
238    if show_stats {
239        let stats = manager.get_statistics()?;
240        println!("📊 Command Usage Statistics");
241        println!("============================");
242        println!("Total commands: {}", stats.total_commands);
243        println!(
244            "Successful: {} ({:.1}%)",
245            stats.total_success,
246            if stats.total_commands > 0 {
247                (stats.total_success as f64 / stats.total_commands as f64) * 100.0
248            } else {
249                0.0
250            }
251        );
252        println!(
253            "Failed: {} ({:.1}%)",
254            stats.total_failed,
255            if stats.total_commands > 0 {
256                (stats.total_failed as f64 / stats.total_commands as f64) * 100.0
257            } else {
258                0.0
259            }
260        );
261        println!();
262
263        if let Some(most_used) = stats.most_used_command {
264            println!(
265                "Most used command: {} ({} times)",
266                most_used, stats.most_used_count
267            );
268        }
269        println!();
270
271        println!("Command breakdown:");
272        let mut cmd_vec: Vec<_> = stats.command_counts.iter().collect();
273        cmd_vec.sort_by(|a, b| b.1.cmp(a.1));
274        for (cmd, count) in cmd_vec.iter().take(10) {
275            println!("  {}: {} times", cmd, count);
276        }
277        return Ok(());
278    }
279
280    if suggest {
281        let suggestions = manager.get_suggestions(limit)?;
282        if suggestions.is_empty() {
283            println!("No suggestions available yet. Use VoiRS more to build history!");
284        } else {
285            println!("💡 Suggested commands based on your usage:");
286            for (i, suggestion) in suggestions.iter().enumerate() {
287                println!("  {}. voirs {}", i + 1, suggestion);
288            }
289        }
290        return Ok(());
291    }
292
293    // Show recent history
294    let entries = manager.get_recent(limit)?;
295    if entries.is_empty() {
296        println!("No command history yet.");
297    } else {
298        println!("📜 Recent command history (last {}):", entries.len());
299        println!();
300        for (i, entry) in entries.iter().rev().enumerate() {
301            let status_icon = match entry.status {
302                ExecutionStatus::Success => "✅",
303                ExecutionStatus::Failed => "❌",
304                ExecutionStatus::Cancelled => "🚫",
305            };
306            println!(
307                "{} {} [{}] voirs {} {}",
308                i + 1,
309                status_icon,
310                entry.timestamp.format("%Y-%m-%d %H:%M:%S"),
311                entry.command,
312                entry.args.join(" ")
313            );
314            if let Some(duration) = entry.duration_ms {
315                println!("      Duration: {}ms", duration);
316            }
317        }
318    }
319
320    Ok(())
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    #[test]
328    fn test_history_entry_serialization() {
329        let entry = HistoryEntry {
330            command: "synthesize".to_string(),
331            args: vec!["test".to_string()],
332            timestamp: Utc::now(),
333            status: ExecutionStatus::Success,
334            duration_ms: Some(1000),
335        };
336
337        let json = serde_json::to_string(&entry).unwrap();
338        let deserialized: HistoryEntry = serde_json::from_str(&json).unwrap();
339
340        assert_eq!(deserialized.command, "synthesize");
341        assert_eq!(deserialized.args, vec!["test"]);
342    }
343
344    #[test]
345    fn test_command_statistics() {
346        let entries = vec![
347            HistoryEntry {
348                command: "synthesize".to_string(),
349                args: vec![],
350                timestamp: Utc::now(),
351                status: ExecutionStatus::Success,
352                duration_ms: None,
353            },
354            HistoryEntry {
355                command: "synthesize".to_string(),
356                args: vec![],
357                timestamp: Utc::now(),
358                status: ExecutionStatus::Success,
359                duration_ms: None,
360            },
361            HistoryEntry {
362                command: "list-voices".to_string(),
363                args: vec![],
364                timestamp: Utc::now(),
365                status: ExecutionStatus::Failed,
366                duration_ms: None,
367            },
368        ];
369
370        let mut command_counts = HashMap::new();
371        for entry in &entries {
372            *command_counts.entry(entry.command.clone()).or_insert(0) += 1;
373        }
374
375        assert_eq!(command_counts.get("synthesize"), Some(&2));
376        assert_eq!(command_counts.get("list-voices"), Some(&1));
377    }
378}