eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
//! Action logging middleware for TEA pattern.
//!
//! Logs every action (Msg) dispatched to the update function.
//! This provides complete observability of state transitions.

use crate::core::Msg;
use std::collections::VecDeque;
use std::time::{Instant, Duration};

/// Records action history for debugging and observability.
pub struct ActionLogger {
    /// Recent actions (ring buffer)
    history: VecDeque<ActionRecord>,
    /// Maximum history size
    max_history: usize,
    /// Start time for relative timestamps
    start_time: Instant,
    /// Whether logging is enabled
    enabled: bool,
}

/// A recorded action with metadata.
#[derive(Debug, Clone)]
pub struct ActionRecord {
    /// The action that was dispatched
    pub action: String,
    /// Time since application start
    pub elapsed: Duration,
    /// Whether the action produced an effect
    pub produced_effect: bool,
}

impl ActionLogger {
    /// Create a new action logger.
    pub fn new(max_history: usize) -> Self {
        Self {
            history: VecDeque::with_capacity(max_history),
            max_history,
            start_time: Instant::now(),
            enabled: true,
        }
    }
    
    /// Log an action being dispatched.
    pub fn log_action(&mut self, msg: &Msg, produced_effect: bool) {
        if !self.enabled {
            return;
        }
        
        let record = ActionRecord {
            action: format!("{:?}", msg),
            elapsed: self.start_time.elapsed(),
            produced_effect,
        };
        
        // Log to tracing for immediate visibility
        if produced_effect {
            tracing::debug!("ACTION [+effect]: {}", record.action);
        } else {
            tracing::trace!("ACTION: {}", record.action);
        }
        
        // Store in ring buffer
        if self.history.len() >= self.max_history {
            self.history.pop_front();
        }
        self.history.push_back(record);
    }
    
    /// Get recent action history.
    pub fn recent_actions(&self, count: usize) -> Vec<&ActionRecord> {
        self.history.iter().rev().take(count).collect()
    }
    
    /// Get all action history.
    pub fn all_actions(&self) -> Vec<&ActionRecord> {
        self.history.iter().collect()
    }
    
    /// Get action count.
    pub fn action_count(&self) -> usize {
        self.history.len()
    }
    
    /// Get counts by action type (for analytics).
    pub fn action_stats(&self) -> std::collections::HashMap<String, usize> {
        let mut stats = std::collections::HashMap::new();
        for record in &self.history {
            // Extract action type name (first word before any payload)
            let action_type = record.action
                .split(|c| c == ' ' || c == '{' || c == '(')
                .next()
                .unwrap_or("Unknown")
                .to_string();
            *stats.entry(action_type).or_insert(0) += 1;
        }
        stats
    }
    
    /// Enable/disable logging.
    pub fn set_enabled(&mut self, enabled: bool) {
        self.enabled = enabled;
    }
    
    /// Clear history.
    pub fn clear(&mut self) {
        self.history.clear();
    }
}

impl Default for ActionLogger {
    fn default() -> Self {
        Self::new(1000) // Keep last 1000 actions
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_action_logging() {
        let mut logger = ActionLogger::new(10);
        
        logger.log_action(&Msg::Tick, false);
        logger.log_action(&Msg::ToggleHelp, false);
        
        assert_eq!(logger.action_count(), 2);
        
        let recent = logger.recent_actions(5);
        assert_eq!(recent.len(), 2);
    }
    
    #[test]
    fn test_ring_buffer() {
        let mut logger = ActionLogger::new(3);
        
        for _ in 0..5 {
            logger.log_action(&Msg::Tick, false);
        }
        
        // Should only keep last 3
        assert_eq!(logger.action_count(), 3);
    }
}