1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::fs;
10use std::path::PathBuf;
11use voirs_sdk::Result;
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct HistoryEntry {
16 pub command: String,
18 pub args: Vec<String>,
20 pub timestamp: DateTime<Utc>,
22 pub status: ExecutionStatus,
24 pub duration_ms: Option<u64>,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
30pub enum ExecutionStatus {
31 Success,
33 Failed,
35 Cancelled,
37}
38
39pub struct HistoryManager {
41 history_path: PathBuf,
43 max_entries: usize,
45}
46
47impl HistoryManager {
48 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 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 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 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 pub fn add_entry(&self, entry: HistoryEntry) -> Result<()> {
106 let mut entries = self.load()?;
107 entries.push(entry);
108
109 if entries.len() > self.max_entries {
111 entries.drain(0..(entries.len() - self.max_entries));
112 }
113
114 self.save(&entries)
115 }
116
117 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 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 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 pub fn get_suggestions(&self, limit: usize) -> Result<Vec<String>> {
163 let entries = self.load()?;
164 let mut suggestions = Vec::new();
165
166 let recent_successful: Vec<_> = entries
168 .iter()
169 .rev()
170 .filter(|e| e.status == ExecutionStatus::Success)
171 .take(limit * 2)
172 .collect();
173
174 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 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 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#[derive(Debug, Default)]
213pub struct CommandStatistics {
214 pub total_commands: usize,
216 pub total_success: usize,
218 pub total_failed: usize,
220 pub command_counts: HashMap<String, usize>,
222 pub most_used_command: Option<String>,
224 pub most_used_count: usize,
226}
227
228pub 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 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}