tui_dispatch_debug/debug/
action_logger.rs1use std::collections::VecDeque;
29use std::time::Instant;
30use tui_dispatch_core::action::ActionParams;
31use tui_dispatch_core::store::Middleware;
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 params_pretty: String,
139 pub timestamp: Instant,
141 pub elapsed: String,
143 pub sequence: u64,
145}
146
147impl ActionLogEntry {
148 pub fn new(name: &'static str, params: String, params_pretty: String, sequence: u64) -> Self {
150 Self {
151 name,
152 params,
153 params_pretty,
154 timestamp: Instant::now(),
155 elapsed: "0ms".to_string(),
156 sequence,
157 }
158 }
159}
160
161fn format_elapsed(elapsed: std::time::Duration) -> String {
163 if elapsed.as_secs() >= 1 {
164 format!("{:.1}s", elapsed.as_secs_f64())
165 } else {
166 format!("{}ms", elapsed.as_millis())
167 }
168}
169
170#[derive(Debug, Clone)]
172pub struct ActionLogConfig {
173 pub capacity: usize,
175 pub filter: ActionLoggerConfig,
177}
178
179impl Default for ActionLogConfig {
180 fn default() -> Self {
181 Self {
182 capacity: 100,
183 filter: ActionLoggerConfig::default(),
184 }
185 }
186}
187
188impl ActionLogConfig {
189 pub fn with_capacity(capacity: usize) -> Self {
191 Self {
192 capacity,
193 ..Default::default()
194 }
195 }
196
197 pub fn new(capacity: usize, filter: ActionLoggerConfig) -> Self {
199 Self { capacity, filter }
200 }
201}
202
203#[derive(Debug, Clone)]
208pub struct ActionLog {
209 entries: VecDeque<ActionLogEntry>,
210 config: ActionLogConfig,
211 next_sequence: u64,
212 start_time: Instant,
214}
215
216impl Default for ActionLog {
217 fn default() -> Self {
218 Self::new(ActionLogConfig::default())
219 }
220}
221
222impl ActionLog {
223 pub fn new(config: ActionLogConfig) -> Self {
225 Self {
226 entries: VecDeque::with_capacity(config.capacity),
227 config,
228 next_sequence: 0,
229 start_time: Instant::now(),
230 }
231 }
232
233 pub fn log<A: ActionParams>(&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 params = action.params();
244 let params_pretty = action.params_pretty();
245 let mut entry = ActionLogEntry::new(name, params, params_pretty, self.next_sequence);
246 entry.elapsed = format_elapsed(self.start_time.elapsed());
248 self.next_sequence += 1;
249
250 if self.entries.len() >= self.config.capacity {
252 self.entries.pop_front();
253 }
254
255 self.entries.push_back(entry);
256 self.entries.back()
257 }
258
259 pub fn entries(&self) -> impl Iterator<Item = &ActionLogEntry> {
261 self.entries.iter()
262 }
263
264 pub fn entries_rev(&self) -> impl Iterator<Item = &ActionLogEntry> {
266 self.entries.iter().rev()
267 }
268
269 pub fn recent(&self, count: usize) -> impl Iterator<Item = &ActionLogEntry> {
271 self.entries.iter().rev().take(count)
272 }
273
274 pub fn len(&self) -> usize {
276 self.entries.len()
277 }
278
279 pub fn is_empty(&self) -> bool {
281 self.entries.is_empty()
282 }
283
284 pub fn clear(&mut self) {
286 self.entries.clear();
287 }
288
289 pub fn config(&self) -> &ActionLogConfig {
291 &self.config
292 }
293
294 pub fn config_mut(&mut self) -> &mut ActionLogConfig {
296 &mut self.config
297 }
298}
299
300#[derive(Debug, Clone)]
330pub struct ActionLoggerMiddleware {
331 config: ActionLoggerConfig,
332 log: Option<ActionLog>,
333 active: bool,
336}
337
338impl ActionLoggerMiddleware {
339 pub fn new(config: ActionLoggerConfig) -> Self {
341 Self {
342 config,
343 log: None,
344 active: true,
345 }
346 }
347
348 pub fn with_log(config: ActionLogConfig) -> Self {
350 Self {
351 config: config.filter.clone(),
352 log: Some(ActionLog::new(config)),
353 active: true,
354 }
355 }
356
357 pub fn with_default_log() -> Self {
359 Self::with_log(ActionLogConfig::default())
360 }
361
362 pub fn default_filtering() -> Self {
364 Self::new(ActionLoggerConfig::default())
365 }
366
367 pub fn log_all() -> Self {
369 Self::new(ActionLoggerConfig::with_patterns(vec![], vec![]))
370 }
371
372 pub fn active(mut self, active: bool) -> Self {
384 self.active = active;
385 self
386 }
387
388 pub fn is_active(&self) -> bool {
390 self.active
391 }
392
393 pub fn log(&self) -> Option<&ActionLog> {
395 self.log.as_ref()
396 }
397
398 pub fn log_mut(&mut self) -> Option<&mut ActionLog> {
400 self.log.as_mut()
401 }
402
403 pub fn config(&self) -> &ActionLoggerConfig {
405 &self.config
406 }
407
408 pub fn config_mut(&mut self) -> &mut ActionLoggerConfig {
410 &mut self.config
411 }
412}
413
414impl<S, A: ActionParams> Middleware<S, A> for ActionLoggerMiddleware {
415 fn before(&mut self, action: &A, _state: &S) -> bool {
416 if !self.active {
418 return true;
419 }
420
421 let name = action.name();
422
423 if self.config.should_log(name) {
425 tracing::debug!(action = %name, "action");
426 }
427
428 if let Some(ref mut log) = self.log {
430 log.log(action);
431 }
432
433 true
434 }
435
436 fn after(&mut self, _action: &A, _state_changed: bool, _state: &S) -> Vec<A> {
437 vec![]
438 }
439}
440
441pub fn glob_match(pattern: &str, text: &str) -> bool {
446 let pattern: Vec<char> = pattern.chars().collect();
447 let text: Vec<char> = text.chars().collect();
448 glob_match_impl(&pattern, &text)
449}
450
451fn glob_match_impl(pattern: &[char], text: &[char]) -> bool {
452 let mut pi = 0;
453 let mut ti = 0;
454 let mut star_pi = None;
455 let mut star_ti = 0;
456
457 while ti < text.len() {
458 if pi < pattern.len() && (pattern[pi] == '?' || pattern[pi] == text[ti]) {
459 pi += 1;
460 ti += 1;
461 } else if pi < pattern.len() && pattern[pi] == '*' {
462 star_pi = Some(pi);
463 star_ti = ti;
464 pi += 1;
465 } else if let Some(spi) = star_pi {
466 pi = spi + 1;
467 star_ti += 1;
468 ti = star_ti;
469 } else {
470 return false;
471 }
472 }
473
474 while pi < pattern.len() && pattern[pi] == '*' {
475 pi += 1;
476 }
477
478 pi == pattern.len()
479}
480
481#[cfg(test)]
482mod tests {
483 use super::*;
484
485 #[test]
486 fn test_glob_match_exact() {
487 assert!(glob_match("Tick", "Tick"));
488 assert!(!glob_match("Tick", "Tock"));
489 assert!(!glob_match("Tick", "TickTock"));
490 }
491
492 #[test]
493 fn test_glob_match_star() {
494 assert!(glob_match("Search*", "SearchAddChar"));
495 assert!(glob_match("Search*", "SearchDeleteChar"));
496 assert!(glob_match("Search*", "Search"));
497 assert!(!glob_match("Search*", "StartSearch"));
498
499 assert!(glob_match("*Search", "StartSearch"));
500 assert!(glob_match("*Search*", "StartSearchNow"));
501
502 assert!(glob_match("Did*", "DidConnect"));
503 assert!(glob_match("Did*", "DidScanKeys"));
504 }
505
506 #[test]
507 fn test_glob_match_question() {
508 assert!(glob_match("Tick?", "Ticks"));
509 assert!(!glob_match("Tick?", "Tick"));
510 assert!(!glob_match("Tick?", "Tickss"));
511 }
512
513 #[test]
514 fn test_glob_match_combined() {
515 assert!(glob_match("*Add*", "SearchAddChar"));
516 assert!(glob_match("Connection*Add*", "ConnectionFormAddChar"));
517 }
518
519 #[test]
520 fn test_action_logger_config_include() {
521 let config = ActionLoggerConfig::new(Some("Search*,Connect"), None);
522 assert!(config.should_log("SearchAddChar"));
523 assert!(config.should_log("Connect"));
524 assert!(!config.should_log("Tick"));
525 assert!(!config.should_log("LoadKeys"));
526 }
527
528 #[test]
529 fn test_action_logger_config_exclude() {
530 let config = ActionLoggerConfig::new(None, Some("Tick,Render,LoadValue*"));
531 assert!(!config.should_log("Tick"));
532 assert!(!config.should_log("Render"));
533 assert!(!config.should_log("LoadValueDebounced"));
534 assert!(config.should_log("SearchAddChar"));
535 assert!(config.should_log("Connect"));
536 }
537
538 #[test]
539 fn test_action_logger_config_include_and_exclude() {
540 let config = ActionLoggerConfig::new(Some("Did*"), Some("DidFail*"));
542 assert!(config.should_log("DidConnect"));
543 assert!(config.should_log("DidScanKeys"));
544 assert!(!config.should_log("DidFailConnect"));
545 assert!(!config.should_log("DidFailScanKeys"));
546 assert!(!config.should_log("SearchAddChar")); }
548
549 #[test]
550 fn test_action_logger_config_default() {
551 let config = ActionLoggerConfig::default();
552 assert!(!config.should_log("Tick"));
553 assert!(!config.should_log("Render"));
554 assert!(config.should_log("Connect"));
555 assert!(config.should_log("SearchAddChar"));
556 }
557
558 #[derive(Clone, Debug)]
560 enum TestAction {
561 Tick,
562 Connect,
563 }
564
565 impl tui_dispatch_core::Action for TestAction {
566 fn name(&self) -> &'static str {
567 match self {
568 TestAction::Tick => "Tick",
569 TestAction::Connect => "Connect",
570 }
571 }
572 }
573
574 impl tui_dispatch_core::ActionParams for TestAction {
575 fn params(&self) -> String {
576 String::new()
577 }
578 }
579
580 #[test]
581 fn test_action_log_basic() {
582 let mut log = ActionLog::default();
583 assert!(log.is_empty());
584
585 log.log(&TestAction::Connect);
586 assert_eq!(log.len(), 1);
587
588 let entry = log.entries().next().unwrap();
589 assert_eq!(entry.name, "Connect");
590 assert_eq!(entry.sequence, 0);
591 }
592
593 #[test]
594 fn test_action_log_filtering() {
595 let mut log = ActionLog::default(); log.log(&TestAction::Tick);
598 assert!(log.is_empty()); log.log(&TestAction::Connect);
601 assert_eq!(log.len(), 1);
602 }
603
604 #[test]
605 fn test_action_log_capacity() {
606 let config = ActionLogConfig::new(
607 3,
608 ActionLoggerConfig::with_patterns(vec![], vec![]), );
610 let mut log = ActionLog::new(config);
611
612 log.log(&TestAction::Connect);
613 log.log(&TestAction::Connect);
614 log.log(&TestAction::Connect);
615 assert_eq!(log.len(), 3);
616
617 log.log(&TestAction::Connect);
618 assert_eq!(log.len(), 3); assert_eq!(log.entries().next().unwrap().sequence, 1);
622 }
623
624 #[test]
625 fn test_action_log_recent() {
626 let config = ActionLogConfig::new(10, ActionLoggerConfig::with_patterns(vec![], vec![]));
627 let mut log = ActionLog::new(config);
628
629 for _ in 0..5 {
630 log.log(&TestAction::Connect);
631 }
632
633 let recent: Vec<_> = log.recent(3).collect();
635 assert_eq!(recent.len(), 3);
636 assert_eq!(recent[0].sequence, 4); assert_eq!(recent[1].sequence, 3);
638 assert_eq!(recent[2].sequence, 2);
639 }
640
641 #[test]
642 fn test_action_log_entry_elapsed() {
643 let entry = ActionLogEntry::new(
644 "Test",
645 "test_params".to_string(),
646 "test_params".to_string(),
647 0,
648 );
649 assert_eq!(entry.elapsed, "0ms");
651 }
652
653 #[test]
654 fn test_middleware_filtering() {
655 use tui_dispatch_core::store::Middleware;
656
657 let mut middleware = ActionLoggerMiddleware::with_default_log();
659
660 middleware.before(&TestAction::Connect, &());
662 middleware.after(&TestAction::Connect, true, &());
663
664 let log = middleware.log().unwrap();
666 assert_eq!(log.len(), 1);
667
668 middleware.before(&TestAction::Tick, &());
670 middleware.after(&TestAction::Tick, false, &());
671
672 let log = middleware.log().unwrap();
674 assert_eq!(log.len(), 1);
675 }
676}