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, DebugLayerBuilder};
133pub use state::{DebugEntry, DebugSection, DebugState, DebugWrapper};
134
135// Action logging
136pub use action_logger::{glob_match, ActionLoggerConfig, ActionLoggerMiddleware};
137
138// Low-level API
139pub use cell::{
140 format_color_compact, format_modifier_compact, inspect_cell, point_in_rect, CellPreview,
141};
142pub use table::{DebugOverlay, DebugTableBuilder, DebugTableOverlay, DebugTableRow};
143pub use widgets::{
144 buffer_to_text, dim_buffer, paint_snapshot, BannerItem, CellPreviewWidget, DebugBanner,
145 DebugTableStyle, DebugTableWidget,
146};
147
148use crate::keybindings::BindingContext;
149use ratatui::buffer::Buffer;
150
151// ============================================================================
152// SimpleDebugContext - Built-in context for simple debug layer usage
153// ============================================================================
154
155/// Built-in context for simple debug layer usage.
156///
157/// Use this with [`DebugLayer::simple()`] for zero-configuration debug layer setup.
158///
159/// # Example
160///
161/// ```ignore
162/// use tui_dispatch::debug::DebugLayer;
163///
164/// // Uses SimpleDebugContext internally:
165/// let debug = DebugLayer::<MyAction>::simple();
166/// ```
167#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
168pub enum SimpleDebugContext {
169 /// Normal application context (not debug mode)
170 #[default]
171 Normal,
172 /// Debug mode context (when freeze is active)
173 Debug,
174}
175
176impl BindingContext for SimpleDebugContext {
177 fn name(&self) -> &'static str {
178 match self {
179 Self::Normal => "normal",
180 Self::Debug => "debug",
181 }
182 }
183
184 fn from_name(name: &str) -> Option<Self> {
185 match name {
186 "normal" => Some(Self::Normal),
187 "debug" => Some(Self::Debug),
188 _ => None,
189 }
190 }
191
192 fn all() -> &'static [Self] {
193 &[Self::Normal, Self::Debug]
194 }
195}
196
197/// Debug freeze state for capturing and inspecting UI frames
198///
199/// Generic over the action type `A` to store queued actions while frozen.
200///
201/// # Example
202///
203/// ```ignore
204/// use tui_dispatch_core::debug::DebugFreeze;
205///
206/// // In your app state:
207/// struct AppState {
208/// debug: DebugFreeze<MyAction>,
209/// // ... other fields
210/// }
211///
212/// // Toggle freeze on F12:
213/// fn handle_action(state: &mut AppState, action: MyAction) {
214/// match action {
215/// MyAction::ToggleDebug => {
216/// state.debug.toggle();
217/// }
218/// other if state.debug.enabled => {
219/// // Queue actions while frozen
220/// state.debug.queue(other);
221/// }
222/// // ... normal handling
223/// }
224/// }
225/// ```
226#[derive(Debug)]
227pub struct DebugFreeze<A> {
228 /// Whether debug/freeze mode is enabled
229 pub enabled: bool,
230 /// Flag to capture the next frame
231 pub pending_capture: bool,
232 /// The captured buffer snapshot
233 pub snapshot: Option<Buffer>,
234 /// Plain text version of snapshot (for clipboard)
235 pub snapshot_text: String,
236 /// Actions queued while frozen
237 pub queued_actions: Vec<A>,
238 /// Feedback message to display
239 pub message: Option<String>,
240 /// Currently displayed overlay
241 pub overlay: Option<DebugOverlay>,
242 /// Whether mouse capture mode is enabled (for position inspection)
243 pub mouse_capture_enabled: bool,
244}
245
246impl<A> Default for DebugFreeze<A> {
247 fn default() -> Self {
248 Self {
249 enabled: false,
250 pending_capture: false,
251 snapshot: None,
252 snapshot_text: String::new(),
253 queued_actions: Vec::new(),
254 message: None,
255 overlay: None,
256 mouse_capture_enabled: false,
257 }
258 }
259}
260
261impl<A> DebugFreeze<A> {
262 /// Create a new debug freeze state
263 pub fn new() -> Self {
264 Self::default()
265 }
266
267 /// Toggle freeze mode on/off
268 ///
269 /// When enabling, sets pending_capture to capture the next frame.
270 /// When disabling, clears the snapshot and queued actions.
271 pub fn toggle(&mut self) {
272 if self.enabled {
273 // Disable
274 self.enabled = false;
275 self.snapshot = None;
276 self.snapshot_text.clear();
277 self.overlay = None;
278 // Note: queued_actions should be processed by the app before clearing
279 } else {
280 // Enable
281 self.enabled = true;
282 self.pending_capture = true;
283 self.queued_actions.clear();
284 }
285 }
286
287 /// Enable freeze mode
288 pub fn enable(&mut self) {
289 if !self.enabled {
290 self.toggle();
291 }
292 }
293
294 /// Disable freeze mode
295 pub fn disable(&mut self) {
296 if self.enabled {
297 self.toggle();
298 }
299 }
300
301 /// Capture the current buffer as a snapshot
302 pub fn capture(&mut self, buffer: &Buffer) {
303 self.snapshot = Some(buffer.clone());
304 self.snapshot_text = buffer_to_text(buffer);
305 self.pending_capture = false;
306 }
307
308 /// Request a new capture on the next frame
309 pub fn request_capture(&mut self) {
310 self.pending_capture = true;
311 }
312
313 /// Queue an action to be processed when freeze is disabled
314 pub fn queue(&mut self, action: A) {
315 self.queued_actions.push(action);
316 }
317
318 /// Take all queued actions, leaving the queue empty
319 pub fn take_queued(&mut self) -> Vec<A> {
320 std::mem::take(&mut self.queued_actions)
321 }
322
323 /// Set a feedback message
324 pub fn set_message(&mut self, msg: impl Into<String>) {
325 self.message = Some(msg.into());
326 }
327
328 /// Clear the feedback message
329 pub fn clear_message(&mut self) {
330 self.message = None;
331 }
332
333 /// Set the current overlay
334 pub fn set_overlay(&mut self, overlay: DebugOverlay) {
335 self.overlay = Some(overlay);
336 }
337
338 /// Clear the current overlay
339 pub fn clear_overlay(&mut self) {
340 self.overlay = None;
341 }
342
343 /// Toggle mouse capture mode
344 pub fn toggle_mouse_capture(&mut self) {
345 self.mouse_capture_enabled = !self.mouse_capture_enabled;
346 }
347}
348
349#[cfg(test)]
350mod tests {
351 use super::*;
352
353 #[derive(Debug, Clone)]
354 enum TestAction {
355 Foo,
356 Bar,
357 }
358
359 #[test]
360 fn test_debug_freeze_toggle() {
361 let mut freeze: DebugFreeze<TestAction> = DebugFreeze::new();
362
363 assert!(!freeze.enabled);
364
365 freeze.toggle();
366 assert!(freeze.enabled);
367 assert!(freeze.pending_capture);
368
369 freeze.toggle();
370 assert!(!freeze.enabled);
371 assert!(freeze.snapshot.is_none());
372 }
373
374 #[test]
375 fn test_debug_freeze_queue() {
376 let mut freeze: DebugFreeze<TestAction> = DebugFreeze::new();
377 freeze.enable();
378
379 freeze.queue(TestAction::Foo);
380 freeze.queue(TestAction::Bar);
381
382 assert_eq!(freeze.queued_actions.len(), 2);
383
384 let queued = freeze.take_queued();
385 assert_eq!(queued.len(), 2);
386 assert!(freeze.queued_actions.is_empty());
387 }
388
389 #[test]
390 fn test_debug_freeze_message() {
391 let mut freeze: DebugFreeze<TestAction> = DebugFreeze::new();
392
393 freeze.set_message("Test message");
394 assert_eq!(freeze.message, Some("Test message".to_string()));
395
396 freeze.clear_message();
397 assert!(freeze.message.is_none());
398 }
399
400 #[test]
401 fn test_simple_debug_context_binding_context() {
402 use crate::keybindings::BindingContext;
403
404 // Test name()
405 assert_eq!(SimpleDebugContext::Normal.name(), "normal");
406 assert_eq!(SimpleDebugContext::Debug.name(), "debug");
407
408 // Test from_name()
409 assert_eq!(
410 SimpleDebugContext::from_name("normal"),
411 Some(SimpleDebugContext::Normal)
412 );
413 assert_eq!(
414 SimpleDebugContext::from_name("debug"),
415 Some(SimpleDebugContext::Debug)
416 );
417 assert_eq!(SimpleDebugContext::from_name("invalid"), None);
418
419 // Test all()
420 let all = SimpleDebugContext::all();
421 assert_eq!(all.len(), 2);
422 assert!(all.contains(&SimpleDebugContext::Normal));
423 assert!(all.contains(&SimpleDebugContext::Debug));
424 }
425
426 #[test]
427 fn test_simple_debug_context_default() {
428 let ctx: SimpleDebugContext = Default::default();
429 assert_eq!(ctx, SimpleDebugContext::Normal);
430 }
431}