tui_dispatch_core/debug/
mod.rs

1//! Debug and inspection utilities for TUI applications
2//!
3//! This module provides tools for debugging TUI applications:
4//!
5//! - **DebugLayer**: High-level wrapper with automatic rendering (recommended)
6//! - **Action Logging**: Pattern-based filtering for action logs
7//! - **Frame Freeze**: Capture and inspect UI state
8//! - **Cell Inspection**: Examine individual buffer cells
9//! - **Debug Widgets**: Render debug overlays and tables
10//!
11//! # Quick Start (Simple API - Recommended)
12//!
13//! ```ignore
14//! use tui_dispatch::debug::DebugLayer;
15//!
16//! // One line setup with sensible defaults:
17//! let debug = DebugLayer::<MyAction>::simple();
18//!
19//! // In render loop:
20//! debug.render(frame, |f, area| {
21//!     render_main_ui(f, area, &state);
22//! });
23//!
24//! // Default keybindings (when debug mode is active):
25//! // - F12/Esc: Toggle debug mode
26//! // - S: Show/hide state overlay
27//! // - Y: Copy frozen frame to clipboard
28//! // - I: Toggle mouse capture for cell inspection
29//! ```
30//!
31//! # Custom Configuration
32//!
33//! ```ignore
34//! use tui_dispatch::debug::{DebugLayer, DebugConfig, DebugAction};
35//!
36//! // Use custom toggle key:
37//! let debug = DebugLayer::<MyAction>::simple_with_toggle_key(&["F11"]);
38//!
39//! // Or full control with custom context:
40//! let config = DebugConfig::new(keybindings, MyContext::Debug);
41//! let debug: DebugLayer<MyAction, MyContext> = DebugLayer::new(config);
42//! ```
43//!
44//! # Manual Control (Escape Hatch)
45//!
46//! ```ignore
47//! // Split area manually
48//! let (app_area, banner_area) = debug.split_area(frame.area());
49//!
50//! // Custom layout
51//! render_my_ui(frame, app_area);
52//!
53//! // Let debug layer render its parts
54//! debug.render_overlay(frame, app_area);
55//! debug.render_banner(frame, banner_area);
56//! ```
57//!
58//! # State Inspection
59//!
60//! Implement `DebugState` for your state types:
61//!
62//! ```ignore
63//! use tui_dispatch::debug::{DebugState, DebugSection};
64//!
65//! impl DebugState for AppState {
66//!     fn debug_sections(&self) -> Vec<DebugSection> {
67//!         vec![
68//!             DebugSection::new("Connection")
69//!                 .entry("host", &self.host)
70//!                 .entry("status", format!("{:?}", self.status)),
71//!         ]
72//!     }
73//! }
74//!
75//! // Then show it:
76//! debug.show_state_overlay(&app_state);
77//! ```
78//!
79//! # Action Logging
80//!
81//! Use [`ActionLoggerMiddleware`] for pattern-based action filtering:
82//!
83//! ```
84//! use tui_dispatch_core::debug::ActionLoggerConfig;
85//!
86//! // Log only Search* and Connect* actions
87//! let config = ActionLoggerConfig::new(Some("Search*,Connect*"), None);
88//!
89//! // Log everything except Tick and Render (default excludes)
90//! let config = ActionLoggerConfig::default();
91//! ```
92//!
93//! # Low-Level API
94//!
95//! For full control, use [`DebugFreeze`] directly:
96//!
97//! ```ignore
98//! use tui_dispatch::debug::{DebugFreeze, paint_snapshot, dim_buffer};
99//!
100//! let debug: DebugFreeze<MyAction> = DebugFreeze::default();
101//!
102//! // In render loop:
103//! if debug.enabled {
104//!     if debug.pending_capture || debug.snapshot.is_none() {
105//!         render_app(f, state);
106//!         debug.capture(f.buffer());
107//!     } else {
108//!         paint_snapshot(f, debug.snapshot.as_ref().unwrap());
109//!     }
110//!     dim_buffer(f.buffer_mut(), 0.7);
111//!     render_debug_overlay(f, &debug);
112//! }
113//! ```
114
115pub mod action_logger;
116pub mod actions;
117pub mod cell;
118pub mod config;
119pub mod layer;
120pub mod state;
121pub mod table;
122pub mod widgets;
123
124// Re-export commonly used types
125
126// High-level API (recommended)
127pub use actions::{DebugAction, DebugSideEffect};
128pub use config::{
129    default_debug_keybindings, default_debug_keybindings_with_toggle, DebugConfig, DebugStyle,
130    KeyStyles, StatusItem,
131};
132pub use layer::DebugLayer;
133pub use state::{DebugEntry, DebugSection, DebugState, DebugWrapper};
134
135// Action logging
136pub use action_logger::{
137    glob_match, ActionLog, ActionLogConfig, ActionLogEntry, ActionLoggerConfig,
138    ActionLoggerMiddleware,
139};
140
141// Low-level API
142pub use cell::{
143    format_color_compact, format_modifier_compact, inspect_cell, point_in_rect, CellPreview,
144};
145pub use table::{
146    ActionLogDisplayEntry, ActionLogOverlay, DebugOverlay, DebugTableBuilder, DebugTableOverlay,
147    DebugTableRow,
148};
149pub use widgets::{
150    buffer_to_text, dim_buffer, paint_snapshot, ActionLogStyle, ActionLogWidget, BannerItem,
151    CellPreviewWidget, DebugBanner, DebugTableStyle, DebugTableWidget,
152};
153
154use crate::keybindings::BindingContext;
155use ratatui::buffer::Buffer;
156
157// ============================================================================
158// SimpleDebugContext - Built-in context for simple debug layer usage
159// ============================================================================
160
161/// Built-in context for simple debug layer usage.
162///
163/// Use this with [`DebugLayer::simple()`] for zero-configuration debug layer setup.
164///
165/// # Example
166///
167/// ```ignore
168/// use tui_dispatch::debug::DebugLayer;
169///
170/// // Uses SimpleDebugContext internally:
171/// let debug = DebugLayer::<MyAction>::simple();
172/// ```
173#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
174pub enum SimpleDebugContext {
175    /// Normal application context (not debug mode)
176    #[default]
177    Normal,
178    /// Debug mode context (when freeze is active)
179    Debug,
180}
181
182impl BindingContext for SimpleDebugContext {
183    fn name(&self) -> &'static str {
184        match self {
185            Self::Normal => "normal",
186            Self::Debug => "debug",
187        }
188    }
189
190    fn from_name(name: &str) -> Option<Self> {
191        match name {
192            "normal" => Some(Self::Normal),
193            "debug" => Some(Self::Debug),
194            _ => None,
195        }
196    }
197
198    fn all() -> &'static [Self] {
199        &[Self::Normal, Self::Debug]
200    }
201}
202
203/// Debug freeze state for capturing and inspecting UI frames
204///
205/// Generic over the action type `A` to store queued actions while frozen.
206///
207/// # Example
208///
209/// ```ignore
210/// use tui_dispatch_core::debug::DebugFreeze;
211///
212/// // In your app state:
213/// struct AppState {
214///     debug: DebugFreeze<MyAction>,
215///     // ... other fields
216/// }
217///
218/// // Toggle freeze on F12:
219/// fn handle_action(state: &mut AppState, action: MyAction) {
220///     match action {
221///         MyAction::ToggleDebug => {
222///             state.debug.toggle();
223///         }
224///         other if state.debug.enabled => {
225///             // Queue actions while frozen
226///             state.debug.queue(other);
227///         }
228///         // ... normal handling
229///     }
230/// }
231/// ```
232#[derive(Debug)]
233pub struct DebugFreeze<A> {
234    /// Whether debug/freeze mode is enabled
235    pub enabled: bool,
236    /// Flag to capture the next frame
237    pub pending_capture: bool,
238    /// The captured buffer snapshot
239    pub snapshot: Option<Buffer>,
240    /// Plain text version of snapshot (for clipboard)
241    pub snapshot_text: String,
242    /// Actions queued while frozen
243    pub queued_actions: Vec<A>,
244    /// Feedback message to display
245    pub message: Option<String>,
246    /// Currently displayed overlay
247    pub overlay: Option<DebugOverlay>,
248    /// Whether mouse capture mode is enabled (for position inspection)
249    pub mouse_capture_enabled: bool,
250}
251
252impl<A> Default for DebugFreeze<A> {
253    fn default() -> Self {
254        Self {
255            enabled: false,
256            pending_capture: false,
257            snapshot: None,
258            snapshot_text: String::new(),
259            queued_actions: Vec::new(),
260            message: None,
261            overlay: None,
262            mouse_capture_enabled: false,
263        }
264    }
265}
266
267impl<A> DebugFreeze<A> {
268    /// Create a new debug freeze state
269    pub fn new() -> Self {
270        Self::default()
271    }
272
273    /// Toggle freeze mode on/off
274    ///
275    /// When enabling, sets pending_capture to capture the next frame.
276    /// When disabling, clears the snapshot and queued actions.
277    pub fn toggle(&mut self) {
278        if self.enabled {
279            // Disable
280            self.enabled = false;
281            self.snapshot = None;
282            self.snapshot_text.clear();
283            self.overlay = None;
284            self.message = None;
285            // Note: queued_actions should be processed by the app before clearing
286        } else {
287            // Enable
288            self.enabled = true;
289            self.pending_capture = true;
290            self.queued_actions.clear();
291            self.message = None;
292        }
293    }
294
295    /// Enable freeze mode
296    pub fn enable(&mut self) {
297        if !self.enabled {
298            self.toggle();
299        }
300    }
301
302    /// Disable freeze mode
303    pub fn disable(&mut self) {
304        if self.enabled {
305            self.toggle();
306        }
307    }
308
309    /// Capture the current buffer as a snapshot
310    pub fn capture(&mut self, buffer: &Buffer) {
311        self.snapshot = Some(buffer.clone());
312        self.snapshot_text = buffer_to_text(buffer);
313        self.pending_capture = false;
314    }
315
316    /// Request a new capture on the next frame
317    pub fn request_capture(&mut self) {
318        self.pending_capture = true;
319    }
320
321    /// Queue an action to be processed when freeze is disabled
322    pub fn queue(&mut self, action: A) {
323        self.queued_actions.push(action);
324    }
325
326    /// Take all queued actions, leaving the queue empty
327    pub fn take_queued(&mut self) -> Vec<A> {
328        std::mem::take(&mut self.queued_actions)
329    }
330
331    /// Set a feedback message
332    pub fn set_message(&mut self, msg: impl Into<String>) {
333        self.message = Some(msg.into());
334    }
335
336    /// Clear the feedback message
337    pub fn clear_message(&mut self) {
338        self.message = None;
339    }
340
341    /// Set the current overlay
342    pub fn set_overlay(&mut self, overlay: DebugOverlay) {
343        self.overlay = Some(overlay);
344    }
345
346    /// Clear the current overlay
347    pub fn clear_overlay(&mut self) {
348        self.overlay = None;
349    }
350
351    /// Toggle mouse capture mode
352    pub fn toggle_mouse_capture(&mut self) {
353        self.mouse_capture_enabled = !self.mouse_capture_enabled;
354    }
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360
361    #[derive(Debug, Clone)]
362    enum TestAction {
363        Foo,
364        Bar,
365    }
366
367    #[test]
368    fn test_debug_freeze_toggle() {
369        let mut freeze: DebugFreeze<TestAction> = DebugFreeze::new();
370
371        assert!(!freeze.enabled);
372
373        freeze.toggle();
374        assert!(freeze.enabled);
375        assert!(freeze.pending_capture);
376
377        freeze.toggle();
378        assert!(!freeze.enabled);
379        assert!(freeze.snapshot.is_none());
380    }
381
382    #[test]
383    fn test_debug_freeze_queue() {
384        let mut freeze: DebugFreeze<TestAction> = DebugFreeze::new();
385        freeze.enable();
386
387        freeze.queue(TestAction::Foo);
388        freeze.queue(TestAction::Bar);
389
390        assert_eq!(freeze.queued_actions.len(), 2);
391
392        let queued = freeze.take_queued();
393        assert_eq!(queued.len(), 2);
394        assert!(freeze.queued_actions.is_empty());
395    }
396
397    #[test]
398    fn test_debug_freeze_message() {
399        let mut freeze: DebugFreeze<TestAction> = DebugFreeze::new();
400
401        freeze.set_message("Test message");
402        assert_eq!(freeze.message, Some("Test message".to_string()));
403
404        freeze.clear_message();
405        assert!(freeze.message.is_none());
406    }
407
408    #[test]
409    fn test_simple_debug_context_binding_context() {
410        use crate::keybindings::BindingContext;
411
412        // Test name()
413        assert_eq!(SimpleDebugContext::Normal.name(), "normal");
414        assert_eq!(SimpleDebugContext::Debug.name(), "debug");
415
416        // Test from_name()
417        assert_eq!(
418            SimpleDebugContext::from_name("normal"),
419            Some(SimpleDebugContext::Normal)
420        );
421        assert_eq!(
422            SimpleDebugContext::from_name("debug"),
423            Some(SimpleDebugContext::Debug)
424        );
425        assert_eq!(SimpleDebugContext::from_name("invalid"), None);
426
427        // Test all()
428        let all = SimpleDebugContext::all();
429        assert_eq!(all.len(), 2);
430        assert!(all.contains(&SimpleDebugContext::Normal));
431        assert!(all.contains(&SimpleDebugContext::Debug));
432    }
433
434    #[test]
435    fn test_simple_debug_context_default() {
436        let ctx: SimpleDebugContext = Default::default();
437        assert_eq!(ctx, SimpleDebugContext::Normal);
438    }
439}