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<A: ActionParams> Middleware<A> for ActionLoggerMiddleware {
415 fn before(&mut self, action: &A) {
416 if !self.active {
418 return;
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
434 fn after(&mut self, _action: &A, _state_changed: bool) {
435 }
437}
438
439pub fn glob_match(pattern: &str, text: &str) -> bool {
444 let pattern: Vec<char> = pattern.chars().collect();
445 let text: Vec<char> = text.chars().collect();
446 glob_match_impl(&pattern, &text)
447}
448
449fn glob_match_impl(pattern: &[char], text: &[char]) -> bool {
450 let mut pi = 0;
451 let mut ti = 0;
452 let mut star_pi = None;
453 let mut star_ti = 0;
454
455 while ti < text.len() {
456 if pi < pattern.len() && (pattern[pi] == '?' || pattern[pi] == text[ti]) {
457 pi += 1;
458 ti += 1;
459 } else if pi < pattern.len() && pattern[pi] == '*' {
460 star_pi = Some(pi);
461 star_ti = ti;
462 pi += 1;
463 } else if let Some(spi) = star_pi {
464 pi = spi + 1;
465 star_ti += 1;
466 ti = star_ti;
467 } else {
468 return false;
469 }
470 }
471
472 while pi < pattern.len() && pattern[pi] == '*' {
473 pi += 1;
474 }
475
476 pi == pattern.len()
477}
478
479#[cfg(test)]
480mod tests {
481 use super::*;
482
483 #[test]
484 fn test_glob_match_exact() {
485 assert!(glob_match("Tick", "Tick"));
486 assert!(!glob_match("Tick", "Tock"));
487 assert!(!glob_match("Tick", "TickTock"));
488 }
489
490 #[test]
491 fn test_glob_match_star() {
492 assert!(glob_match("Search*", "SearchAddChar"));
493 assert!(glob_match("Search*", "SearchDeleteChar"));
494 assert!(glob_match("Search*", "Search"));
495 assert!(!glob_match("Search*", "StartSearch"));
496
497 assert!(glob_match("*Search", "StartSearch"));
498 assert!(glob_match("*Search*", "StartSearchNow"));
499
500 assert!(glob_match("Did*", "DidConnect"));
501 assert!(glob_match("Did*", "DidScanKeys"));
502 }
503
504 #[test]
505 fn test_glob_match_question() {
506 assert!(glob_match("Tick?", "Ticks"));
507 assert!(!glob_match("Tick?", "Tick"));
508 assert!(!glob_match("Tick?", "Tickss"));
509 }
510
511 #[test]
512 fn test_glob_match_combined() {
513 assert!(glob_match("*Add*", "SearchAddChar"));
514 assert!(glob_match("Connection*Add*", "ConnectionFormAddChar"));
515 }
516
517 #[test]
518 fn test_action_logger_config_include() {
519 let config = ActionLoggerConfig::new(Some("Search*,Connect"), None);
520 assert!(config.should_log("SearchAddChar"));
521 assert!(config.should_log("Connect"));
522 assert!(!config.should_log("Tick"));
523 assert!(!config.should_log("LoadKeys"));
524 }
525
526 #[test]
527 fn test_action_logger_config_exclude() {
528 let config = ActionLoggerConfig::new(None, Some("Tick,Render,LoadValue*"));
529 assert!(!config.should_log("Tick"));
530 assert!(!config.should_log("Render"));
531 assert!(!config.should_log("LoadValueDebounced"));
532 assert!(config.should_log("SearchAddChar"));
533 assert!(config.should_log("Connect"));
534 }
535
536 #[test]
537 fn test_action_logger_config_include_and_exclude() {
538 let config = ActionLoggerConfig::new(Some("Did*"), Some("DidFail*"));
540 assert!(config.should_log("DidConnect"));
541 assert!(config.should_log("DidScanKeys"));
542 assert!(!config.should_log("DidFailConnect"));
543 assert!(!config.should_log("DidFailScanKeys"));
544 assert!(!config.should_log("SearchAddChar")); }
546
547 #[test]
548 fn test_action_logger_config_default() {
549 let config = ActionLoggerConfig::default();
550 assert!(!config.should_log("Tick"));
551 assert!(!config.should_log("Render"));
552 assert!(config.should_log("Connect"));
553 assert!(config.should_log("SearchAddChar"));
554 }
555
556 #[derive(Clone, Debug)]
558 enum TestAction {
559 Tick,
560 Connect,
561 }
562
563 impl tui_dispatch_core::Action for TestAction {
564 fn name(&self) -> &'static str {
565 match self {
566 TestAction::Tick => "Tick",
567 TestAction::Connect => "Connect",
568 }
569 }
570 }
571
572 impl tui_dispatch_core::ActionParams for TestAction {
573 fn params(&self) -> String {
574 String::new()
575 }
576 }
577
578 #[test]
579 fn test_action_log_basic() {
580 let mut log = ActionLog::default();
581 assert!(log.is_empty());
582
583 log.log(&TestAction::Connect);
584 assert_eq!(log.len(), 1);
585
586 let entry = log.entries().next().unwrap();
587 assert_eq!(entry.name, "Connect");
588 assert_eq!(entry.sequence, 0);
589 }
590
591 #[test]
592 fn test_action_log_filtering() {
593 let mut log = ActionLog::default(); log.log(&TestAction::Tick);
596 assert!(log.is_empty()); log.log(&TestAction::Connect);
599 assert_eq!(log.len(), 1);
600 }
601
602 #[test]
603 fn test_action_log_capacity() {
604 let config = ActionLogConfig::new(
605 3,
606 ActionLoggerConfig::with_patterns(vec![], vec![]), );
608 let mut log = ActionLog::new(config);
609
610 log.log(&TestAction::Connect);
611 log.log(&TestAction::Connect);
612 log.log(&TestAction::Connect);
613 assert_eq!(log.len(), 3);
614
615 log.log(&TestAction::Connect);
616 assert_eq!(log.len(), 3); assert_eq!(log.entries().next().unwrap().sequence, 1);
620 }
621
622 #[test]
623 fn test_action_log_recent() {
624 let config = ActionLogConfig::new(10, ActionLoggerConfig::with_patterns(vec![], vec![]));
625 let mut log = ActionLog::new(config);
626
627 for _ in 0..5 {
628 log.log(&TestAction::Connect);
629 }
630
631 let recent: Vec<_> = log.recent(3).collect();
633 assert_eq!(recent.len(), 3);
634 assert_eq!(recent[0].sequence, 4); assert_eq!(recent[1].sequence, 3);
636 assert_eq!(recent[2].sequence, 2);
637 }
638
639 #[test]
640 fn test_action_log_entry_elapsed() {
641 let entry = ActionLogEntry::new(
642 "Test",
643 "test_params".to_string(),
644 "test_params".to_string(),
645 0,
646 );
647 assert_eq!(entry.elapsed, "0ms");
649 }
650
651 #[test]
652 fn test_middleware_filtering() {
653 use tui_dispatch_core::store::Middleware;
654
655 let mut middleware = ActionLoggerMiddleware::with_default_log();
657
658 middleware.before(&TestAction::Connect);
660 middleware.after(&TestAction::Connect, true);
661
662 let log = middleware.log().unwrap();
664 assert_eq!(log.len(), 1);
665
666 middleware.before(&TestAction::Tick);
668 middleware.after(&TestAction::Tick, false);
669
670 let log = middleware.log().unwrap();
672 assert_eq!(log.len(), 1);
673 }
674}