1use crate::action::ActionParams;
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 params: String,
137 pub timestamp: Instant,
139 pub elapsed: String,
141 pub sequence: u64,
143}
144
145impl ActionLogEntry {
146 pub fn new(name: &'static str, params: String, sequence: u64) -> Self {
148 Self {
149 name,
150 params,
151 timestamp: Instant::now(),
152 elapsed: "0ms".to_string(),
153 sequence,
154 }
155 }
156}
157
158fn format_elapsed(elapsed: std::time::Duration) -> String {
160 if elapsed.as_secs() >= 1 {
161 format!("{:.1}s", elapsed.as_secs_f64())
162 } else {
163 format!("{}ms", elapsed.as_millis())
164 }
165}
166
167#[derive(Debug, Clone)]
169pub struct ActionLogConfig {
170 pub capacity: usize,
172 pub filter: ActionLoggerConfig,
174}
175
176impl Default for ActionLogConfig {
177 fn default() -> Self {
178 Self {
179 capacity: 100,
180 filter: ActionLoggerConfig::default(),
181 }
182 }
183}
184
185impl ActionLogConfig {
186 pub fn with_capacity(capacity: usize) -> Self {
188 Self {
189 capacity,
190 ..Default::default()
191 }
192 }
193
194 pub fn new(capacity: usize, filter: ActionLoggerConfig) -> Self {
196 Self { capacity, filter }
197 }
198}
199
200#[derive(Debug, Clone)]
205pub struct ActionLog {
206 entries: VecDeque<ActionLogEntry>,
207 config: ActionLogConfig,
208 next_sequence: u64,
209 start_time: Instant,
211}
212
213impl Default for ActionLog {
214 fn default() -> Self {
215 Self::new(ActionLogConfig::default())
216 }
217}
218
219impl ActionLog {
220 pub fn new(config: ActionLogConfig) -> Self {
222 Self {
223 entries: VecDeque::with_capacity(config.capacity),
224 config,
225 next_sequence: 0,
226 start_time: Instant::now(),
227 }
228 }
229
230 pub fn log<A: ActionParams>(&mut self, action: &A) -> Option<&ActionLogEntry> {
234 let name = action.name();
235
236 if !self.config.filter.should_log(name) {
237 return None;
238 }
239
240 let params = action.params();
241 let mut entry = ActionLogEntry::new(name, params, self.next_sequence);
242 entry.elapsed = format_elapsed(self.start_time.elapsed());
244 self.next_sequence += 1;
245
246 if self.entries.len() >= self.config.capacity {
248 self.entries.pop_front();
249 }
250
251 self.entries.push_back(entry);
252 self.entries.back()
253 }
254
255 pub fn entries(&self) -> impl Iterator<Item = &ActionLogEntry> {
257 self.entries.iter()
258 }
259
260 pub fn entries_rev(&self) -> impl Iterator<Item = &ActionLogEntry> {
262 self.entries.iter().rev()
263 }
264
265 pub fn recent(&self, count: usize) -> impl Iterator<Item = &ActionLogEntry> {
267 self.entries.iter().rev().take(count)
268 }
269
270 pub fn len(&self) -> usize {
272 self.entries.len()
273 }
274
275 pub fn is_empty(&self) -> bool {
277 self.entries.is_empty()
278 }
279
280 pub fn clear(&mut self) {
282 self.entries.clear();
283 }
284
285 pub fn config(&self) -> &ActionLogConfig {
287 &self.config
288 }
289
290 pub fn config_mut(&mut self) -> &mut ActionLogConfig {
292 &mut self.config
293 }
294}
295
296#[derive(Debug, Clone)]
326pub struct ActionLoggerMiddleware {
327 config: ActionLoggerConfig,
328 log: Option<ActionLog>,
329 active: bool,
332}
333
334impl ActionLoggerMiddleware {
335 pub fn new(config: ActionLoggerConfig) -> Self {
337 Self {
338 config,
339 log: None,
340 active: true,
341 }
342 }
343
344 pub fn with_log(config: ActionLogConfig) -> Self {
346 Self {
347 config: config.filter.clone(),
348 log: Some(ActionLog::new(config)),
349 active: true,
350 }
351 }
352
353 pub fn with_default_log() -> Self {
355 Self::with_log(ActionLogConfig::default())
356 }
357
358 pub fn default_filtering() -> Self {
360 Self::new(ActionLoggerConfig::default())
361 }
362
363 pub fn log_all() -> Self {
365 Self::new(ActionLoggerConfig::with_patterns(vec![], vec![]))
366 }
367
368 pub fn active(mut self, active: bool) -> Self {
380 self.active = active;
381 self
382 }
383
384 pub fn is_active(&self) -> bool {
386 self.active
387 }
388
389 pub fn log(&self) -> Option<&ActionLog> {
391 self.log.as_ref()
392 }
393
394 pub fn log_mut(&mut self) -> Option<&mut ActionLog> {
396 self.log.as_mut()
397 }
398
399 pub fn config(&self) -> &ActionLoggerConfig {
401 &self.config
402 }
403
404 pub fn config_mut(&mut self) -> &mut ActionLoggerConfig {
406 &mut self.config
407 }
408}
409
410impl<A: ActionParams> Middleware<A> for ActionLoggerMiddleware {
411 fn before(&mut self, action: &A) {
412 if !self.active {
414 return;
415 }
416
417 let name = action.name();
418
419 if self.config.should_log(name) {
421 tracing::debug!(action = %name, "action");
422 }
423
424 if let Some(ref mut log) = self.log {
426 log.log(action);
427 }
428 }
429
430 fn after(&mut self, _action: &A, _state_changed: bool) {
431 }
433}
434
435pub fn glob_match(pattern: &str, text: &str) -> bool {
440 let pattern: Vec<char> = pattern.chars().collect();
441 let text: Vec<char> = text.chars().collect();
442 glob_match_impl(&pattern, &text)
443}
444
445fn glob_match_impl(pattern: &[char], text: &[char]) -> bool {
446 let mut pi = 0;
447 let mut ti = 0;
448 let mut star_pi = None;
449 let mut star_ti = 0;
450
451 while ti < text.len() {
452 if pi < pattern.len() && (pattern[pi] == '?' || pattern[pi] == text[ti]) {
453 pi += 1;
454 ti += 1;
455 } else if pi < pattern.len() && pattern[pi] == '*' {
456 star_pi = Some(pi);
457 star_ti = ti;
458 pi += 1;
459 } else if let Some(spi) = star_pi {
460 pi = spi + 1;
461 star_ti += 1;
462 ti = star_ti;
463 } else {
464 return false;
465 }
466 }
467
468 while pi < pattern.len() && pattern[pi] == '*' {
469 pi += 1;
470 }
471
472 pi == pattern.len()
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478
479 #[test]
480 fn test_glob_match_exact() {
481 assert!(glob_match("Tick", "Tick"));
482 assert!(!glob_match("Tick", "Tock"));
483 assert!(!glob_match("Tick", "TickTock"));
484 }
485
486 #[test]
487 fn test_glob_match_star() {
488 assert!(glob_match("Search*", "SearchAddChar"));
489 assert!(glob_match("Search*", "SearchDeleteChar"));
490 assert!(glob_match("Search*", "Search"));
491 assert!(!glob_match("Search*", "StartSearch"));
492
493 assert!(glob_match("*Search", "StartSearch"));
494 assert!(glob_match("*Search*", "StartSearchNow"));
495
496 assert!(glob_match("Did*", "DidConnect"));
497 assert!(glob_match("Did*", "DidScanKeys"));
498 }
499
500 #[test]
501 fn test_glob_match_question() {
502 assert!(glob_match("Tick?", "Ticks"));
503 assert!(!glob_match("Tick?", "Tick"));
504 assert!(!glob_match("Tick?", "Tickss"));
505 }
506
507 #[test]
508 fn test_glob_match_combined() {
509 assert!(glob_match("*Add*", "SearchAddChar"));
510 assert!(glob_match("Connection*Add*", "ConnectionFormAddChar"));
511 }
512
513 #[test]
514 fn test_action_logger_config_include() {
515 let config = ActionLoggerConfig::new(Some("Search*,Connect"), None);
516 assert!(config.should_log("SearchAddChar"));
517 assert!(config.should_log("Connect"));
518 assert!(!config.should_log("Tick"));
519 assert!(!config.should_log("LoadKeys"));
520 }
521
522 #[test]
523 fn test_action_logger_config_exclude() {
524 let config = ActionLoggerConfig::new(None, Some("Tick,Render,LoadValue*"));
525 assert!(!config.should_log("Tick"));
526 assert!(!config.should_log("Render"));
527 assert!(!config.should_log("LoadValueDebounced"));
528 assert!(config.should_log("SearchAddChar"));
529 assert!(config.should_log("Connect"));
530 }
531
532 #[test]
533 fn test_action_logger_config_include_and_exclude() {
534 let config = ActionLoggerConfig::new(Some("Did*"), Some("DidFail*"));
536 assert!(config.should_log("DidConnect"));
537 assert!(config.should_log("DidScanKeys"));
538 assert!(!config.should_log("DidFailConnect"));
539 assert!(!config.should_log("DidFailScanKeys"));
540 assert!(!config.should_log("SearchAddChar")); }
542
543 #[test]
544 fn test_action_logger_config_default() {
545 let config = ActionLoggerConfig::default();
546 assert!(!config.should_log("Tick"));
547 assert!(!config.should_log("Render"));
548 assert!(config.should_log("Connect"));
549 assert!(config.should_log("SearchAddChar"));
550 }
551
552 #[derive(Clone, Debug)]
554 enum TestAction {
555 Tick,
556 Connect,
557 }
558
559 impl crate::Action for TestAction {
560 fn name(&self) -> &'static str {
561 match self {
562 TestAction::Tick => "Tick",
563 TestAction::Connect => "Connect",
564 }
565 }
566 }
567
568 impl crate::ActionParams for TestAction {
569 fn params(&self) -> String {
570 String::new()
571 }
572 }
573
574 #[test]
575 fn test_action_log_basic() {
576 let mut log = ActionLog::default();
577 assert!(log.is_empty());
578
579 log.log(&TestAction::Connect);
580 assert_eq!(log.len(), 1);
581
582 let entry = log.entries().next().unwrap();
583 assert_eq!(entry.name, "Connect");
584 assert_eq!(entry.sequence, 0);
585 }
586
587 #[test]
588 fn test_action_log_filtering() {
589 let mut log = ActionLog::default(); log.log(&TestAction::Tick);
592 assert!(log.is_empty()); log.log(&TestAction::Connect);
595 assert_eq!(log.len(), 1);
596 }
597
598 #[test]
599 fn test_action_log_capacity() {
600 let config = ActionLogConfig::new(
601 3,
602 ActionLoggerConfig::with_patterns(vec![], vec![]), );
604 let mut log = ActionLog::new(config);
605
606 log.log(&TestAction::Connect);
607 log.log(&TestAction::Connect);
608 log.log(&TestAction::Connect);
609 assert_eq!(log.len(), 3);
610
611 log.log(&TestAction::Connect);
612 assert_eq!(log.len(), 3); assert_eq!(log.entries().next().unwrap().sequence, 1);
616 }
617
618 #[test]
619 fn test_action_log_recent() {
620 let config = ActionLogConfig::new(10, ActionLoggerConfig::with_patterns(vec![], vec![]));
621 let mut log = ActionLog::new(config);
622
623 for _ in 0..5 {
624 log.log(&TestAction::Connect);
625 }
626
627 let recent: Vec<_> = log.recent(3).collect();
629 assert_eq!(recent.len(), 3);
630 assert_eq!(recent[0].sequence, 4); assert_eq!(recent[1].sequence, 3);
632 assert_eq!(recent[2].sequence, 2);
633 }
634
635 #[test]
636 fn test_action_log_entry_elapsed() {
637 let entry = ActionLogEntry::new("Test", "test_params".to_string(), 0);
638 assert_eq!(entry.elapsed, "0ms");
640 }
641
642 #[test]
643 fn test_middleware_filtering() {
644 use crate::store::Middleware;
645
646 let mut middleware = ActionLoggerMiddleware::with_default_log();
648
649 middleware.before(&TestAction::Connect);
651 middleware.after(&TestAction::Connect, true);
652
653 let log = middleware.log().unwrap();
655 assert_eq!(log.len(), 1);
656
657 middleware.before(&TestAction::Tick);
659 middleware.after(&TestAction::Tick, false);
660
661 let log = middleware.log().unwrap();
663 assert_eq!(log.len(), 1);
664 }
665}