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}