Skip to main content

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