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