this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 477 lines 16 kB view raw
1use std::collections::VecDeque; 2use std::fs; 3use std::io::{BufRead, BufReader, Write}; 4use std::path::Path; 5use serde::{Serialize, Deserialize}; 6 7/// Command history management with persistence and search 8#[derive(Debug)] 9pub struct CommandHistory { 10 commands: VecDeque<HistoryEntry>, 11 max_size: usize, 12 current_index: Option<usize>, 13 search_mode: bool, 14 search_query: String, 15 filtered_commands: Vec<usize>, 16} 17 18impl CommandHistory { 19 pub fn new(max_size: usize) -> Self { 20 Self { 21 commands: VecDeque::with_capacity(max_size), 22 max_size, 23 current_index: None, 24 search_mode: false, 25 search_query: String::new(), 26 filtered_commands: Vec::new(), 27 } 28 } 29 30 /// Add a command to history 31 pub fn add_command(&mut self, command: String) { 32 // Don't add empty commands or duplicates 33 if command.trim().is_empty() { 34 return; 35 } 36 37 // Don't add if it's the same as the last command 38 if let Some(last) = self.commands.back() { 39 if last.command == command { 40 return; 41 } 42 } 43 44 let entry = HistoryEntry { 45 command, 46 timestamp: chrono::Utc::now(), 47 success: None, // Will be updated later 48 duration: None, 49 }; 50 51 self.commands.push_back(entry); 52 53 // Maintain max size 54 if self.commands.len() > self.max_size { 55 self.commands.pop_front(); 56 } 57 58 // Reset navigation 59 self.current_index = None; 60 self.exit_search_mode(); 61 } 62 63 /// Update the last command's execution result 64 pub fn update_last_result(&mut self, success: bool, duration: std::time::Duration) { 65 if let Some(last) = self.commands.back_mut() { 66 last.success = Some(success); 67 last.duration = Some(duration); 68 } 69 } 70 71 /// Get previous command in history 72 pub fn get_previous(&mut self) -> Option<String> { 73 if self.commands.is_empty() { 74 return None; 75 } 76 77 if self.search_mode { 78 return self.get_previous_filtered(); 79 } 80 81 let new_index = match self.current_index { 82 None => self.commands.len() - 1, 83 Some(0) => return None, // Already at the beginning 84 Some(i) => i - 1, 85 }; 86 87 self.current_index = Some(new_index); 88 Some(self.commands[new_index].command.clone()) 89 } 90 91 /// Get next command in history 92 pub fn get_next(&mut self) -> Option<String> { 93 if self.commands.is_empty() { 94 return None; 95 } 96 97 if self.search_mode { 98 return self.get_next_filtered(); 99 } 100 101 let new_index = match self.current_index { 102 None => return None, 103 Some(i) if i >= self.commands.len() - 1 => { 104 self.current_index = None; 105 return Some(String::new()); // Return empty string to clear input 106 } 107 Some(i) => i + 1, 108 }; 109 110 self.current_index = Some(new_index); 111 Some(self.commands[new_index].command.clone()) 112 } 113 114 /// Search history by substring 115 pub fn search(&mut self, query: &str) -> Vec<String> { 116 let query_lower = query.to_lowercase(); 117 let mut results = Vec::new(); 118 119 for entry in self.commands.iter().rev() { 120 if entry.command.to_lowercase().contains(&query_lower) { 121 results.push(entry.command.clone()); 122 if results.len() >= 20 { 123 break; 124 } 125 } 126 } 127 128 results 129 } 130 131 /// Enter search mode 132 pub fn enter_search_mode(&mut self, query: String) { 133 self.search_mode = true; 134 self.search_query = query.to_lowercase(); 135 self.update_filtered_commands(); 136 self.current_index = None; 137 } 138 139 /// Exit search mode 140 pub fn exit_search_mode(&mut self) { 141 self.search_mode = false; 142 self.search_query.clear(); 143 self.filtered_commands.clear(); 144 self.current_index = None; 145 } 146 147 /// Update search query 148 pub fn update_search_query(&mut self, query: String) { 149 self.search_query = query.to_lowercase(); 150 self.update_filtered_commands(); 151 self.current_index = None; 152 } 153 154 /// Get recent commands 155 pub fn get_recent_commands(&self, count: usize) -> Vec<String> { 156 self.commands 157 .iter() 158 .rev() 159 .take(count) 160 .map(|entry| entry.command.clone()) 161 .collect() 162 } 163 164 /// Get all commands 165 pub fn get_all_commands(&self) -> Vec<&HistoryEntry> { 166 self.commands.iter().collect() 167 } 168 169 /// Get commands by pattern 170 pub fn get_commands_by_pattern(&self, pattern: &str) -> Vec<String> { 171 let pattern_lower = pattern.to_lowercase(); 172 let mut results = Vec::new(); 173 174 for entry in &self.commands { 175 if entry.command.to_lowercase().contains(&pattern_lower) { 176 results.push(entry.command.clone()); 177 } 178 } 179 180 results 181 } 182 183 /// Get command statistics 184 pub fn get_statistics(&self) -> HistoryStatistics { 185 let total_commands = self.commands.len(); 186 let successful_commands = self.commands.iter() 187 .filter(|entry| entry.success == Some(true)) 188 .count(); 189 let failed_commands = self.commands.iter() 190 .filter(|entry| entry.success == Some(false)) 191 .count(); 192 193 let total_duration: std::time::Duration = self.commands.iter() 194 .filter_map(|entry| entry.duration) 195 .sum(); 196 197 let average_duration = if total_commands > 0 { 198 total_duration / total_commands as u32 199 } else { 200 std::time::Duration::ZERO 201 }; 202 203 // Find most common commands 204 let mut command_counts = std::collections::HashMap::new(); 205 for entry in &self.commands { 206 let words: Vec<&str> = entry.command.split_whitespace().collect(); 207 if let Some(first_word) = words.first() { 208 *command_counts.entry(first_word.to_uppercase()).or_insert(0) += 1; 209 } 210 } 211 212 let mut most_common: Vec<_> = command_counts.into_iter().collect(); 213 most_common.sort_by(|a, b| b.1.cmp(&a.1)); 214 most_common.truncate(5); 215 216 HistoryStatistics { 217 total_commands, 218 successful_commands, 219 failed_commands, 220 average_duration, 221 most_common_commands: most_common.into_iter().map(|(cmd, count)| (cmd.to_string(), count)).collect(), 222 } 223 } 224 225 /// Clear all history 226 pub fn clear(&mut self) { 227 self.commands.clear(); 228 self.current_index = None; 229 self.exit_search_mode(); 230 } 231 232 /// Save history to file 233 pub fn save_to_file(&self, filename: &str) -> Result<(), std::io::Error> { 234 let mut file = std::fs::File::create(filename)?; 235 236 for entry in &self.commands { 237 let line = format!("{}\n", entry.command); 238 file.write_all(line.as_bytes())?; 239 } 240 241 Ok(()) 242 } 243 244 /// Load history from file 245 pub fn load_from_file(&mut self, filename: &str) -> Result<(), std::io::Error> { 246 if !Path::new(filename).exists() { 247 return Ok(()); // File doesn't exist, that's fine 248 } 249 250 let file = std::fs::File::open(filename)?; 251 let reader = BufReader::new(file); 252 253 for line in reader.lines() { 254 let command = line?.trim().to_string(); 255 if !command.is_empty() { 256 self.add_command(command); 257 } 258 } 259 260 Ok(()) 261 } 262 263 /// Export history as JSON 264 pub fn export_json(&self, filename: &str) -> Result<(), Box<dyn std::error::Error>> { 265 let json_data = serde_json::to_string_pretty(&self.commands.iter().collect::<Vec<_>>())?; 266 fs::write(filename, json_data)?; 267 Ok(()) 268 } 269 270 /// Import history from JSON 271 pub fn import_json(&mut self, filename: &str) -> Result<usize, Box<dyn std::error::Error>> { 272 let content = fs::read_to_string(filename)?; 273 let entries: Vec<HistoryEntry> = serde_json::from_str(&content)?; 274 275 let imported_count = entries.len(); 276 for entry in entries { 277 self.commands.push_back(entry); 278 } 279 280 // Maintain max size 281 while self.commands.len() > self.max_size { 282 self.commands.pop_front(); 283 } 284 285 Ok(imported_count) 286 } 287 288 // Private helper methods 289 290 fn update_filtered_commands(&mut self) { 291 self.filtered_commands.clear(); 292 293 for (index, entry) in self.commands.iter().enumerate() { 294 if entry.command.to_lowercase().contains(&self.search_query) { 295 self.filtered_commands.push(index); 296 } 297 } 298 } 299 300 fn get_previous_filtered(&mut self) -> Option<String> { 301 if self.filtered_commands.is_empty() { 302 return None; 303 } 304 305 let filtered_index = match self.current_index { 306 None => self.filtered_commands.len() - 1, 307 Some(current_real_index) => { 308 // Find current position in filtered list 309 let current_filtered_pos = self.filtered_commands.iter() 310 .position(|&idx| idx == current_real_index)?; 311 312 if current_filtered_pos == 0 { 313 return None; // Already at beginning 314 } 315 316 current_filtered_pos - 1 317 } 318 }; 319 320 let real_index = self.filtered_commands[filtered_index]; 321 self.current_index = Some(real_index); 322 Some(self.commands[real_index].command.clone()) 323 } 324 325 fn get_next_filtered(&mut self) -> Option<String> { 326 if self.filtered_commands.is_empty() { 327 return None; 328 } 329 330 let filtered_index = match self.current_index { 331 None => return None, 332 Some(current_real_index) => { 333 // Find current position in filtered list 334 let current_filtered_pos = self.filtered_commands.iter() 335 .position(|&idx| idx == current_real_index)?; 336 337 if current_filtered_pos >= self.filtered_commands.len() - 1 { 338 self.current_index = None; 339 return Some(String::new()); // Clear input 340 } 341 342 current_filtered_pos + 1 343 } 344 }; 345 346 let real_index = self.filtered_commands[filtered_index]; 347 self.current_index = Some(real_index); 348 Some(self.commands[real_index].command.clone()) 349 } 350} 351 352/// History entry with metadata 353#[derive(Debug, Clone, Serialize, Deserialize)] 354pub struct HistoryEntry { 355 pub command: String, 356 pub timestamp: chrono::DateTime<chrono::Utc>, 357 pub success: Option<bool>, 358 pub duration: Option<std::time::Duration>, 359} 360 361/// History statistics 362#[derive(Debug)] 363pub struct HistoryStatistics { 364 pub total_commands: usize, 365 pub successful_commands: usize, 366 pub failed_commands: usize, 367 pub average_duration: std::time::Duration, 368 pub most_common_commands: Vec<(String, usize)>, 369} 370 371impl std::fmt::Display for HistoryStatistics { 372 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 373 writeln!(f, "History Statistics:")?; 374 writeln!(f, " Total commands: {}", self.total_commands)?; 375 writeln!(f, " Successful: {}", self.successful_commands)?; 376 writeln!(f, " Failed: {}", self.failed_commands)?; 377 writeln!(f, " Average duration: {:?}", self.average_duration)?; 378 writeln!(f, " Most common commands:")?; 379 for (cmd, count) in &self.most_common_commands { 380 writeln!(f, " {}: {} times", cmd, count)?; 381 } 382 Ok(()) 383 } 384} 385 386#[cfg(test)] 387mod tests { 388 use super::*; 389 use std::time::Duration; 390 391 #[test] 392 fn test_history_basic_operations() { 393 let mut history = CommandHistory::new(10); 394 395 history.add_command("MATCH (n) RETURN n".to_string()); 396 history.add_command("CREATE (n:Person)".to_string()); 397 398 assert_eq!(history.commands.len(), 2); 399 400 let prev = history.get_previous(); 401 assert_eq!(prev, Some("CREATE (n:Person)".to_string())); 402 403 let prev2 = history.get_previous(); 404 assert_eq!(prev2, Some("MATCH (n) RETURN n".to_string())); 405 } 406 407 #[test] 408 fn test_history_search() { 409 let mut history = CommandHistory::new(10); 410 411 history.add_command("MATCH (n) RETURN n".to_string()); 412 history.add_command("CREATE (n:Person)".to_string()); 413 history.add_command("MATCH (p:Person) RETURN p".to_string()); 414 415 let results = history.search("MATCH"); 416 assert_eq!(results.len(), 2); 417 assert!(results.contains(&"MATCH (p:Person) RETURN p".to_string())); 418 assert!(results.contains(&"MATCH (n) RETURN n".to_string())); 419 } 420 421 #[test] 422 fn test_history_deduplication() { 423 let mut history = CommandHistory::new(10); 424 425 history.add_command("MATCH (n) RETURN n".to_string()); 426 history.add_command("MATCH (n) RETURN n".to_string()); // Duplicate 427 428 assert_eq!(history.commands.len(), 1); 429 } 430 431 #[test] 432 fn test_history_max_size() { 433 let mut history = CommandHistory::new(3); 434 435 history.add_command("command1".to_string()); 436 history.add_command("command2".to_string()); 437 history.add_command("command3".to_string()); 438 history.add_command("command4".to_string()); // Should remove command1 439 440 assert_eq!(history.commands.len(), 3); 441 assert!(!history.commands.iter().any(|e| e.command == "command1")); 442 assert!(history.commands.iter().any(|e| e.command == "command4")); 443 } 444 445 #[test] 446 fn test_history_statistics() { 447 let mut history = CommandHistory::new(10); 448 449 history.add_command("MATCH (n) RETURN n".to_string()); 450 history.update_last_result(true, Duration::from_millis(100)); 451 452 history.add_command("CREATE (n:Person)".to_string()); 453 history.update_last_result(false, Duration::from_millis(50)); 454 455 let stats = history.get_statistics(); 456 assert_eq!(stats.total_commands, 2); 457 assert_eq!(stats.successful_commands, 1); 458 assert_eq!(stats.failed_commands, 1); 459 } 460 461 #[test] 462 fn test_search_mode() { 463 let mut history = CommandHistory::new(10); 464 465 history.add_command("MATCH (n) RETURN n".to_string()); 466 history.add_command("CREATE (n:Person)".to_string()); 467 history.add_command("MATCH (p:Person) RETURN p".to_string()); 468 469 history.enter_search_mode("MATCH".to_string()); 470 assert!(history.search_mode); 471 assert_eq!(history.filtered_commands.len(), 2); 472 473 let prev = history.get_previous(); 474 assert!(prev.is_some()); 475 assert!(prev.unwrap().contains("MATCH")); 476 } 477}