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 (DebugLayer - Recommended)
12//!
13//! ```ignore
14//! use tui_dispatch::debug::{DebugLayer, DebugConfig, DebugAction};
15//!
16//! // In your app:
17//! let config = DebugConfig::new(keybindings, MyContext::Debug);
18//! let debug: DebugLayer<MyAction, MyContext> = DebugLayer::new(config);
19//!
20//! // In render loop - automatic handling:
21//! debug.render(frame, |f, area| {
22//!     render_main_ui(f, area, &state);
23//! });
24//!
25//! // In event loop:
26//! if let Some(debug_action) = DebugAction::from_command(&cmd) {
27//!     if let Some(side_effect) = debug.handle_action(debug_action) {
28//!         // Handle clipboard, mouse capture, etc.
29//!     }
30//! }
31//! ```
32//!
33//! # Manual Control (Escape Hatch)
34//!
35//! ```ignore
36//! // Split area manually
37//! let (app_area, banner_area) = debug.split_area(frame.area());
38//!
39//! // Custom layout
40//! render_my_ui(frame, app_area);
41//!
42//! // Let debug layer render its parts
43//! debug.render_overlay(frame, app_area);
44//! debug.render_banner(frame, banner_area);
45//! ```
46//!
47//! # State Inspection
48//!
49//! Implement `DebugState` for your state types:
50//!
51//! ```ignore
52//! use tui_dispatch::debug::{DebugState, DebugSection};
53//!
54//! impl DebugState for AppState {
55//!     fn debug_sections(&self) -> Vec<DebugSection> {
56//!         vec![
57//!             DebugSection::new("Connection")
58//!                 .entry("host", &self.host)
59//!                 .entry("status", format!("{:?}", self.status)),
60//!         ]
61//!     }
62//! }
63//!
64//! // Then show it:
65//! debug.show_state_overlay(&app_state);
66//! ```
67//!
68//! # Action Logging
69//!
70//! Use [`ActionLoggerMiddleware`] for pattern-based action filtering:
71//!
72//! ```
73//! use tui_dispatch_core::debug::ActionLoggerConfig;
74//!
75//! // Log only Search* and Connect* actions
76//! let config = ActionLoggerConfig::new(Some("Search*,Connect*"), None);
77//!
78//! // Log everything except Tick and Render (default excludes)
79//! let config = ActionLoggerConfig::default();
80//! ```
81//!
82//! # Low-Level API
83//!
84//! For full control, use [`DebugFreeze`] directly:
85//!
86//! ```ignore
87//! use tui_dispatch::debug::{DebugFreeze, paint_snapshot, dim_buffer};
88//!
89//! let debug: DebugFreeze<MyAction> = DebugFreeze::default();
90//!
91//! // In render loop:
92//! if debug.enabled {
93//!     if debug.pending_capture || debug.snapshot.is_none() {
94//!         render_app(f, state);
95//!         debug.capture(f.buffer());
96//!     } else {
97//!         paint_snapshot(f, debug.snapshot.as_ref().unwrap());
98//!     }
99//!     dim_buffer(f.buffer_mut(), 0.7);
100//!     render_debug_overlay(f, &debug);
101//! }
102//! ```
103
104pub mod action_logger;
105pub mod actions;
106pub mod cell;
107pub mod config;
108pub mod layer;
109pub mod state;
110pub mod table;
111pub mod widgets;
112
113// Re-export commonly used types
114
115// High-level API (recommended)
116pub use actions::{DebugAction, DebugSideEffect};
117pub use config::{DebugConfig, DebugStyle, StatusItem};
118pub use layer::{DebugLayer, DebugLayerBuilder};
119pub use state::{DebugEntry, DebugSection, DebugState, DebugWrapper};
120
121// Action logging
122pub use action_logger::{glob_match, ActionLoggerConfig, ActionLoggerMiddleware};
123
124// Low-level API
125pub use cell::{
126    format_color_compact, format_modifier_compact, inspect_cell, point_in_rect, CellPreview,
127};
128pub use table::{DebugOverlay, DebugTableBuilder, DebugTableOverlay, DebugTableRow};
129pub use widgets::{
130    buffer_to_text, dim_buffer, paint_snapshot, BannerItem, CellPreviewWidget, DebugBanner,
131    DebugTableStyle, DebugTableWidget,
132};
133
134use ratatui::buffer::Buffer;
135
136/// Debug freeze state for capturing and inspecting UI frames
137///
138/// Generic over the action type `A` to store queued actions while frozen.
139///
140/// # Example
141///
142/// ```ignore
143/// use tui_dispatch_core::debug::DebugFreeze;
144///
145/// // In your app state:
146/// struct AppState {
147///     debug: DebugFreeze<MyAction>,
148///     // ... other fields
149/// }
150///
151/// // Toggle freeze on F12:
152/// fn handle_action(state: &mut AppState, action: MyAction) {
153///     match action {
154///         MyAction::ToggleDebug => {
155///             state.debug.toggle();
156///         }
157///         other if state.debug.enabled => {
158///             // Queue actions while frozen
159///             state.debug.queue(other);
160///         }
161///         // ... normal handling
162///     }
163/// }
164/// ```
165#[derive(Debug)]
166pub struct DebugFreeze<A> {
167    /// Whether debug/freeze mode is enabled
168    pub enabled: bool,
169    /// Flag to capture the next frame
170    pub pending_capture: bool,
171    /// The captured buffer snapshot
172    pub snapshot: Option<Buffer>,
173    /// Plain text version of snapshot (for clipboard)
174    pub snapshot_text: String,
175    /// Actions queued while frozen
176    pub queued_actions: Vec<A>,
177    /// Feedback message to display
178    pub message: Option<String>,
179    /// Currently displayed overlay
180    pub overlay: Option<DebugOverlay>,
181    /// Whether mouse capture mode is enabled (for position inspection)
182    pub mouse_capture_enabled: bool,
183}
184
185impl<A> Default for DebugFreeze<A> {
186    fn default() -> Self {
187        Self {
188            enabled: false,
189            pending_capture: false,
190            snapshot: None,
191            snapshot_text: String::new(),
192            queued_actions: Vec::new(),
193            message: None,
194            overlay: None,
195            mouse_capture_enabled: false,
196        }
197    }
198}
199
200impl<A> DebugFreeze<A> {
201    /// Create a new debug freeze state
202    pub fn new() -> Self {
203        Self::default()
204    }
205
206    /// Toggle freeze mode on/off
207    ///
208    /// When enabling, sets pending_capture to capture the next frame.
209    /// When disabling, clears the snapshot and queued actions.
210    pub fn toggle(&mut self) {
211        if self.enabled {
212            // Disable
213            self.enabled = false;
214            self.snapshot = None;
215            self.snapshot_text.clear();
216            self.overlay = None;
217            // Note: queued_actions should be processed by the app before clearing
218        } else {
219            // Enable
220            self.enabled = true;
221            self.pending_capture = true;
222            self.queued_actions.clear();
223        }
224    }
225
226    /// Enable freeze mode
227    pub fn enable(&mut self) {
228        if !self.enabled {
229            self.toggle();
230        }
231    }
232
233    /// Disable freeze mode
234    pub fn disable(&mut self) {
235        if self.enabled {
236            self.toggle();
237        }
238    }
239
240    /// Capture the current buffer as a snapshot
241    pub fn capture(&mut self, buffer: &Buffer) {
242        self.snapshot = Some(buffer.clone());
243        self.snapshot_text = buffer_to_text(buffer);
244        self.pending_capture = false;
245    }
246
247    /// Request a new capture on the next frame
248    pub fn request_capture(&mut self) {
249        self.pending_capture = true;
250    }
251
252    /// Queue an action to be processed when freeze is disabled
253    pub fn queue(&mut self, action: A) {
254        self.queued_actions.push(action);
255    }
256
257    /// Take all queued actions, leaving the queue empty
258    pub fn take_queued(&mut self) -> Vec<A> {
259        std::mem::take(&mut self.queued_actions)
260    }
261
262    /// Set a feedback message
263    pub fn set_message(&mut self, msg: impl Into<String>) {
264        self.message = Some(msg.into());
265    }
266
267    /// Clear the feedback message
268    pub fn clear_message(&mut self) {
269        self.message = None;
270    }
271
272    /// Set the current overlay
273    pub fn set_overlay(&mut self, overlay: DebugOverlay) {
274        self.overlay = Some(overlay);
275    }
276
277    /// Clear the current overlay
278    pub fn clear_overlay(&mut self) {
279        self.overlay = None;
280    }
281
282    /// Toggle mouse capture mode
283    pub fn toggle_mouse_capture(&mut self) {
284        self.mouse_capture_enabled = !self.mouse_capture_enabled;
285    }
286}
287
288#[cfg(test)]
289mod tests {
290    use super::*;
291
292    #[derive(Debug, Clone)]
293    enum TestAction {
294        Foo,
295        Bar,
296    }
297
298    #[test]
299    fn test_debug_freeze_toggle() {
300        let mut freeze: DebugFreeze<TestAction> = DebugFreeze::new();
301
302        assert!(!freeze.enabled);
303
304        freeze.toggle();
305        assert!(freeze.enabled);
306        assert!(freeze.pending_capture);
307
308        freeze.toggle();
309        assert!(!freeze.enabled);
310        assert!(freeze.snapshot.is_none());
311    }
312
313    #[test]
314    fn test_debug_freeze_queue() {
315        let mut freeze: DebugFreeze<TestAction> = DebugFreeze::new();
316        freeze.enable();
317
318        freeze.queue(TestAction::Foo);
319        freeze.queue(TestAction::Bar);
320
321        assert_eq!(freeze.queued_actions.len(), 2);
322
323        let queued = freeze.take_queued();
324        assert_eq!(queued.len(), 2);
325        assert!(freeze.queued_actions.is_empty());
326    }
327
328    #[test]
329    fn test_debug_freeze_message() {
330        let mut freeze: DebugFreeze<TestAction> = DebugFreeze::new();
331
332        freeze.set_message("Test message");
333        assert_eq!(freeze.message, Some("Test message".to_string()));
334
335        freeze.clear_message();
336        assert!(freeze.message.is_none());
337    }
338}