1use crate::action::ActionSummary;
29use crate::store::Middleware;
30use std::collections::VecDeque;
31use std::time::Instant;
32
33#[derive(Debug, Clone)]
47pub struct ActionLoggerConfig {
48 pub include_patterns: Vec<String>,
50 pub exclude_patterns: Vec<String>,
52}
53
54impl Default for ActionLoggerConfig {
55 fn default() -> Self {
56 Self {
57 include_patterns: Vec::new(),
58 exclude_patterns: vec!["Tick".to_string(), "Render".to_string()],
60 }
61 }
62}
63
64impl ActionLoggerConfig {
65 pub fn new(include: Option<&str>, exclude: Option<&str>) -> Self {
81 let include_patterns = include
82 .map(|s| s.split(',').map(|p| p.trim().to_string()).collect())
83 .unwrap_or_default();
84
85 let exclude_patterns = exclude
86 .map(|s| s.split(',').map(|p| p.trim().to_string()).collect())
87 .unwrap_or_else(|| vec!["Tick".to_string(), "Render".to_string()]);
88
89 Self {
90 include_patterns,
91 exclude_patterns,
92 }
93 }
94
95 pub fn with_patterns(include: Vec<String>, exclude: Vec<String>) -> Self {
97 Self {
98 include_patterns: include,
99 exclude_patterns: exclude,
100 }
101 }
102
103 pub fn should_log(&self, action_name: &str) -> bool {
105 if !self.include_patterns.is_empty() {
107 let matches_include = self
108 .include_patterns
109 .iter()
110 .any(|p| glob_match(p, action_name));
111 if !matches_include {
112 return false;
113 }
114 }
115
116 let matches_exclude = self
118 .exclude_patterns
119 .iter()
120 .any(|p| glob_match(p, action_name));
121
122 !matches_exclude
123 }
124}
125
126#[derive(Debug, Clone)]
132pub struct ActionLogEntry {
133 pub name: &'static str,
135 pub summary: String,
137 pub timestamp: Instant,
139 pub sequence: u64,
141 pub state_changed: Option<bool>,
143}
144
145impl ActionLogEntry {
146 pub fn new(name: &'static str, summary: String, sequence: u64) -> Self {
148 Self {
149 name,
150 summary,
151 timestamp: Instant::now(),
152 sequence,
153 state_changed: None,
154 }
155 }
156
157 pub fn elapsed(&self) -> std::time::Duration {
159 self.timestamp.elapsed()
160 }
161
162 pub fn elapsed_display(&self) -> String {
164 let elapsed = self.elapsed();
165 if elapsed.as_secs() >= 1 {
166 format!("{:.1}s", elapsed.as_secs_f64())
167 } else {
168 format!("{}ms", elapsed.as_millis())
169 }
170 }
171}
172
173#[derive(Debug, Clone)]
175pub struct ActionLogConfig {
176 pub capacity: usize,
178 pub filter: ActionLoggerConfig,
180}
181
182impl Default for ActionLogConfig {
183 fn default() -> Self {
184 Self {
185 capacity: 100,
186 filter: ActionLoggerConfig::default(),
187 }
188 }
189}
190
191impl ActionLogConfig {
192 pub fn with_capacity(capacity: usize) -> Self {
194 Self {
195 capacity,
196 ..Default::default()
197 }
198 }
199
200 pub fn new(capacity: usize, filter: ActionLoggerConfig) -> Self {
202 Self { capacity, filter }
203 }
204}
205
206#[derive(Debug, Clone)]
211pub struct ActionLog {
212 entries: VecDeque<ActionLogEntry>,
213 config: ActionLogConfig,
214 next_sequence: u64,
215}
216
217impl Default for ActionLog {
218 fn default() -> Self {
219 Self::new(ActionLogConfig::default())
220 }
221}
222
223impl ActionLog {
224 pub fn new(config: ActionLogConfig) -> Self {
226 Self {
227 entries: VecDeque::with_capacity(config.capacity),
228 config,
229 next_sequence: 0,
230 }
231 }
232
233 pub fn log<A: ActionSummary>(&mut self, action: &A) -> Option<&ActionLogEntry> {
237 let name = action.name();
238
239 if !self.config.filter.should_log(name) {
240 return None;
241 }
242
243 let summary = action.summary();
244 let entry = ActionLogEntry::new(name, summary, self.next_sequence);
245 self.next_sequence += 1;
246
247 if self.entries.len() >= self.config.capacity {
249 self.entries.pop_front();
250 }
251
252 self.entries.push_back(entry);
253 self.entries.back()
254 }
255
256 pub fn update_last_state_changed(&mut self, changed: bool) {
258 if let Some(entry) = self.entries.back_mut() {
259 entry.state_changed = Some(changed);
260 }
261 }
262
263 pub fn entries(&self) -> impl Iterator<Item = &ActionLogEntry> {
265 self.entries.iter()
266 }
267
268 pub fn entries_rev(&self) -> impl Iterator<Item = &ActionLogEntry> {
270 self.entries.iter().rev()
271 }
272
273 pub fn recent(&self, count: usize) -> impl Iterator<Item = &ActionLogEntry> {
275 self.entries.iter().rev().take(count)
276 }
277
278 pub fn len(&self) -> usize {
280 self.entries.len()
281 }
282
283 pub fn is_empty(&self) -> bool {
285 self.entries.is_empty()
286 }
287
288 pub fn clear(&mut self) {
290 self.entries.clear();
291 }
292
293 pub fn config(&self) -> &ActionLogConfig {
295 &self.config
296 }
297
298 pub fn config_mut(&mut self) -> &mut ActionLogConfig {
300 &mut self.config
301 }
302}
303
304#[derive(Debug, Clone)]
334pub struct ActionLoggerMiddleware {
335 config: ActionLoggerConfig,
336 log: Option<ActionLog>,
337 last_action_logged: bool,
339}
340
341impl ActionLoggerMiddleware {
342 pub fn new(config: ActionLoggerConfig) -> Self {
344 Self {
345 config,
346 log: None,
347 last_action_logged: false,
348 }
349 }
350
351 pub fn with_log(config: ActionLogConfig) -> Self {
353 Self {
354 config: config.filter.clone(),
355 log: Some(ActionLog::new(config)),
356 last_action_logged: false,
357 }
358 }
359
360 pub fn with_default_log() -> Self {
362 Self::with_log(ActionLogConfig::default())
363 }
364
365 pub fn default_filtering() -> Self {
367 Self::new(ActionLoggerConfig::default())
368 }
369
370 pub fn log_all() -> Self {
372 Self::new(ActionLoggerConfig::with_patterns(vec![], vec![]))
373 }
374
375 pub fn log(&self) -> Option<&ActionLog> {
377 self.log.as_ref()
378 }
379
380 pub fn log_mut(&mut self) -> Option<&mut ActionLog> {
382 self.log.as_mut()
383 }
384
385 pub fn config(&self) -> &ActionLoggerConfig {
387 &self.config
388 }
389
390 pub fn config_mut(&mut self) -> &mut ActionLoggerConfig {
392 &mut self.config
393 }
394}
395
396impl<A: ActionSummary> Middleware<A> for ActionLoggerMiddleware {
397 fn before(&mut self, action: &A) {
398 let name = action.name();
399
400 if self.config.should_log(name) {
402 tracing::debug!(action = %name, "action");
403 }
404
405 self.last_action_logged = false;
407 if let Some(ref mut log) = self.log {
408 if log.log(action).is_some() {
409 self.last_action_logged = true;
410 }
411 }
412 }
413
414 fn after(&mut self, _action: &A, state_changed: bool) {
415 if self.last_action_logged {
417 if let Some(ref mut log) = self.log {
418 log.update_last_state_changed(state_changed);
419 }
420 }
421 }
422}
423
424pub fn glob_match(pattern: &str, text: &str) -> bool {
429 let pattern: Vec<char> = pattern.chars().collect();
430 let text: Vec<char> = text.chars().collect();
431 glob_match_impl(&pattern, &text)
432}
433
434fn glob_match_impl(pattern: &[char], text: &[char]) -> bool {
435 let mut pi = 0;
436 let mut ti = 0;
437 let mut star_pi = None;
438 let mut star_ti = 0;
439
440 while ti < text.len() {
441 if pi < pattern.len() && (pattern[pi] == '?' || pattern[pi] == text[ti]) {
442 pi += 1;
443 ti += 1;
444 } else if pi < pattern.len() && pattern[pi] == '*' {
445 star_pi = Some(pi);
446 star_ti = ti;
447 pi += 1;
448 } else if let Some(spi) = star_pi {
449 pi = spi + 1;
450 star_ti += 1;
451 ti = star_ti;
452 } else {
453 return false;
454 }
455 }
456
457 while pi < pattern.len() && pattern[pi] == '*' {
458 pi += 1;
459 }
460
461 pi == pattern.len()
462}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 #[test]
469 fn test_glob_match_exact() {
470 assert!(glob_match("Tick", "Tick"));
471 assert!(!glob_match("Tick", "Tock"));
472 assert!(!glob_match("Tick", "TickTock"));
473 }
474
475 #[test]
476 fn test_glob_match_star() {
477 assert!(glob_match("Search*", "SearchAddChar"));
478 assert!(glob_match("Search*", "SearchDeleteChar"));
479 assert!(glob_match("Search*", "Search"));
480 assert!(!glob_match("Search*", "StartSearch"));
481
482 assert!(glob_match("*Search", "StartSearch"));
483 assert!(glob_match("*Search*", "StartSearchNow"));
484
485 assert!(glob_match("Did*", "DidConnect"));
486 assert!(glob_match("Did*", "DidScanKeys"));
487 }
488
489 #[test]
490 fn test_glob_match_question() {
491 assert!(glob_match("Tick?", "Ticks"));
492 assert!(!glob_match("Tick?", "Tick"));
493 assert!(!glob_match("Tick?", "Tickss"));
494 }
495
496 #[test]
497 fn test_glob_match_combined() {
498 assert!(glob_match("*Add*", "SearchAddChar"));
499 assert!(glob_match("Connection*Add*", "ConnectionFormAddChar"));
500 }
501
502 #[test]
503 fn test_action_logger_config_include() {
504 let config = ActionLoggerConfig::new(Some("Search*,Connect"), None);
505 assert!(config.should_log("SearchAddChar"));
506 assert!(config.should_log("Connect"));
507 assert!(!config.should_log("Tick"));
508 assert!(!config.should_log("LoadKeys"));
509 }
510
511 #[test]
512 fn test_action_logger_config_exclude() {
513 let config = ActionLoggerConfig::new(None, Some("Tick,Render,LoadValue*"));
514 assert!(!config.should_log("Tick"));
515 assert!(!config.should_log("Render"));
516 assert!(!config.should_log("LoadValueDebounced"));
517 assert!(config.should_log("SearchAddChar"));
518 assert!(config.should_log("Connect"));
519 }
520
521 #[test]
522 fn test_action_logger_config_include_and_exclude() {
523 let config = ActionLoggerConfig::new(Some("Did*"), Some("DidFail*"));
525 assert!(config.should_log("DidConnect"));
526 assert!(config.should_log("DidScanKeys"));
527 assert!(!config.should_log("DidFailConnect"));
528 assert!(!config.should_log("DidFailScanKeys"));
529 assert!(!config.should_log("SearchAddChar")); }
531
532 #[test]
533 fn test_action_logger_config_default() {
534 let config = ActionLoggerConfig::default();
535 assert!(!config.should_log("Tick"));
536 assert!(!config.should_log("Render"));
537 assert!(config.should_log("Connect"));
538 assert!(config.should_log("SearchAddChar"));
539 }
540
541 #[derive(Clone, Debug)]
543 enum TestAction {
544 Tick,
545 Connect,
546 }
547
548 impl crate::Action for TestAction {
549 fn name(&self) -> &'static str {
550 match self {
551 TestAction::Tick => "Tick",
552 TestAction::Connect => "Connect",
553 }
554 }
555 }
556
557 impl crate::ActionSummary for TestAction {}
559
560 #[test]
561 fn test_action_log_basic() {
562 let mut log = ActionLog::default();
563 assert!(log.is_empty());
564
565 log.log(&TestAction::Connect);
566 assert_eq!(log.len(), 1);
567
568 let entry = log.entries().next().unwrap();
569 assert_eq!(entry.name, "Connect");
570 assert_eq!(entry.sequence, 0);
571 }
572
573 #[test]
574 fn test_action_log_filtering() {
575 let mut log = ActionLog::default(); log.log(&TestAction::Tick);
578 assert!(log.is_empty()); log.log(&TestAction::Connect);
581 assert_eq!(log.len(), 1);
582 }
583
584 #[test]
585 fn test_action_log_capacity() {
586 let config = ActionLogConfig::new(
587 3,
588 ActionLoggerConfig::with_patterns(vec![], vec![]), );
590 let mut log = ActionLog::new(config);
591
592 log.log(&TestAction::Connect);
593 log.log(&TestAction::Connect);
594 log.log(&TestAction::Connect);
595 assert_eq!(log.len(), 3);
596
597 log.log(&TestAction::Connect);
598 assert_eq!(log.len(), 3); assert_eq!(log.entries().next().unwrap().sequence, 1);
602 }
603
604 #[test]
605 fn test_action_log_state_changed() {
606 let mut log = ActionLog::default();
607
608 log.log(&TestAction::Connect);
609 log.update_last_state_changed(true);
610
611 let entry = log.entries().next().unwrap();
612 assert_eq!(entry.state_changed, Some(true));
613 }
614
615 #[test]
616 fn test_action_log_recent() {
617 let config = ActionLogConfig::new(10, ActionLoggerConfig::with_patterns(vec![], vec![]));
618 let mut log = ActionLog::new(config);
619
620 for _ in 0..5 {
621 log.log(&TestAction::Connect);
622 }
623
624 let recent: Vec<_> = log.recent(3).collect();
626 assert_eq!(recent.len(), 3);
627 assert_eq!(recent[0].sequence, 4); assert_eq!(recent[1].sequence, 3);
629 assert_eq!(recent[2].sequence, 2);
630 }
631
632 #[test]
633 fn test_action_log_entry_elapsed_display() {
634 let entry = ActionLogEntry::new("Test", "Test".to_string(), 0);
635 let display = entry.elapsed_display();
637 assert!(display.ends_with("ms") || display.ends_with("s"));
638 }
639
640 #[test]
641 fn test_middleware_filtered_action_does_not_update_state_changed() {
642 use crate::store::Middleware;
643
644 let mut middleware = ActionLoggerMiddleware::with_default_log();
646
647 middleware.before(&TestAction::Connect);
649 middleware.after(&TestAction::Connect, true);
650
651 let log = middleware.log().unwrap();
653 assert_eq!(log.len(), 1);
654 assert_eq!(log.entries().next().unwrap().state_changed, Some(true));
655
656 middleware.before(&TestAction::Tick);
658 middleware.after(&TestAction::Tick, false);
659
660 let log = middleware.log().unwrap();
662 assert_eq!(log.len(), 1);
663
664 assert_eq!(log.entries().next().unwrap().state_changed, Some(true));
667 }
668}