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