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}