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//! // - /: 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 scroll_state;
129pub mod state;
130pub mod table;
131pub mod widgets;
132
133// Re-export commonly used types
134
135// High-level API (recommended)
136pub use actions::{DebugAction, DebugSideEffect};
137pub use config::{
138    default_debug_keybindings, default_debug_keybindings_with_toggle, DebugConfig, DebugStyle,
139    KeyStyles, ScrollbarStyle, StatusItem,
140};
141pub use layer::{BannerPosition, DebugLayer, DebugOutcome};
142pub use state::{DebugEntry, DebugSection, DebugState, DebugWrapper};
143
144// Action logging
145pub use action_logger::{
146    glob_match, ActionLog, ActionLogConfig, ActionLogEntry, ActionLoggerConfig,
147    ActionLoggerMiddleware,
148};
149
150// Scroll/cursor utilities
151pub use scroll_state::ScrollState;
152
153// Low-level API
154pub use cell::{
155    format_color_compact, format_modifier_compact, inspect_cell, point_in_rect, CellPreview,
156};
157pub use format::{debug_string, debug_string_compact, debug_string_pretty, pretty_reformat};
158pub use table::{
159    ActionLogDisplayEntry, ActionLogOverlay, ComponentDetailOverlay, ComponentSnapshot,
160    ComponentsOverlay, DebugOverlay, DebugTableBuilder, DebugTableOverlay, DebugTableRow,
161    StateEntryDetail,
162};
163pub use widgets::{
164    buffer_to_text, dim_buffer, paint_snapshot, ActionLogStyle, ActionLogWidget, BannerItem,
165    CellPreviewWidget, DebugBanner, DebugTableStyle, DebugTableWidget,
166};
167
168use ratatui::buffer::Buffer;
169use tui_dispatch_core::keybindings::BindingContext;
170use tui_dispatch_core::runtime::{DebugAdapter, RenderContext};
171use tui_dispatch_core::{Action, ActionParams};
172
173// ============================================================================
174// SimpleDebugContext - Built-in context for simple debug layer usage
175// ============================================================================
176
177/// Built-in context for default debug keybindings.
178///
179/// Use this with [`default_debug_keybindings`] or
180/// [`default_debug_keybindings_with_toggle`] when wiring debug commands into
181/// your own keybinding system.
182///
183/// # Example
184///
185/// ```ignore
186/// use tui_dispatch_debug::debug::default_debug_keybindings;
187///
188/// let keybindings = default_debug_keybindings();
189/// ```
190#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
191pub enum SimpleDebugContext {
192    /// Normal application context (not debug mode)
193    #[default]
194    Normal,
195    /// Debug mode context (when freeze is active)
196    Debug,
197}
198
199impl BindingContext for SimpleDebugContext {
200    fn name(&self) -> &'static str {
201        match self {
202            Self::Normal => "normal",
203            Self::Debug => "debug",
204        }
205    }
206
207    fn from_name(name: &str) -> Option<Self> {
208        match name {
209            "normal" => Some(Self::Normal),
210            "debug" => Some(Self::Debug),
211            _ => None,
212        }
213    }
214
215    fn all() -> &'static [Self] {
216        &[Self::Normal, Self::Debug]
217    }
218}
219
220/// Debug freeze state for capturing and inspecting UI frames
221///
222/// Generic over the action type `A` to store queued actions while frozen.
223///
224/// # Example
225///
226/// ```ignore
227/// use tui_dispatch_debug::debug::DebugFreeze;
228///
229/// // In your app state:
230/// struct AppState {
231///     debug: DebugFreeze<MyAction>,
232///     // ... other fields
233/// }
234///
235/// // Toggle freeze on F12:
236/// fn handle_action(state: &mut AppState, action: MyAction) {
237///     match action {
238///         MyAction::ToggleDebug => {
239///             state.debug.toggle();
240///         }
241///         other if state.debug.enabled => {
242///             // Queue actions while frozen
243///             state.debug.queue(other);
244///         }
245///         // ... normal handling
246///     }
247/// }
248/// ```
249#[derive(Debug)]
250pub struct DebugFreeze<A> {
251    /// Whether debug/freeze mode is enabled
252    pub enabled: bool,
253    /// Flag to capture the next frame
254    pub pending_capture: bool,
255    /// The captured buffer snapshot
256    pub snapshot: Option<Buffer>,
257    /// Plain text version of snapshot (for clipboard)
258    pub snapshot_text: String,
259    /// Actions queued while frozen
260    pub queued_actions: Vec<A>,
261    /// Feedback message to display
262    pub message: Option<String>,
263    /// Currently displayed overlay
264    pub overlay: Option<DebugOverlay>,
265    /// Whether mouse capture mode is enabled (for position inspection)
266    pub mouse_capture_enabled: bool,
267}
268
269impl<A> Default for DebugFreeze<A> {
270    fn default() -> Self {
271        Self {
272            enabled: false,
273            pending_capture: false,
274            snapshot: None,
275            snapshot_text: String::new(),
276            queued_actions: Vec::new(),
277            message: None,
278            overlay: None,
279            mouse_capture_enabled: false,
280        }
281    }
282}
283
284impl<A> DebugFreeze<A> {
285    /// Create a new debug freeze state
286    pub fn new() -> Self {
287        Self::default()
288    }
289
290    /// Toggle freeze mode on/off
291    ///
292    /// When enabling, sets pending_capture to capture the next frame.
293    /// When disabling, clears the snapshot and queued actions.
294    pub fn toggle(&mut self) {
295        if self.enabled {
296            // Disable
297            self.enabled = false;
298            self.snapshot = None;
299            self.snapshot_text.clear();
300            self.overlay = None;
301            self.message = None;
302            // Note: queued_actions should be processed by the app before clearing
303        } else {
304            // Enable
305            self.enabled = true;
306            self.pending_capture = true;
307            self.queued_actions.clear();
308            self.message = None;
309        }
310    }
311
312    /// Enable freeze mode
313    pub fn enable(&mut self) {
314        if !self.enabled {
315            self.toggle();
316        }
317    }
318
319    /// Disable freeze mode
320    pub fn disable(&mut self) {
321        if self.enabled {
322            self.toggle();
323        }
324    }
325
326    /// Capture the current buffer as a snapshot
327    pub fn capture(&mut self, buffer: &Buffer) {
328        self.snapshot = Some(buffer.clone());
329        self.snapshot_text = buffer_to_text(buffer);
330        self.pending_capture = false;
331    }
332
333    /// Request a new capture on the next frame
334    pub fn request_capture(&mut self) {
335        self.pending_capture = true;
336    }
337
338    /// Queue an action to be processed when freeze is disabled
339    pub fn queue(&mut self, action: A) {
340        self.queued_actions.push(action);
341    }
342
343    /// Take all queued actions, leaving the queue empty
344    pub fn take_queued(&mut self) -> Vec<A> {
345        std::mem::take(&mut self.queued_actions)
346    }
347
348    /// Set a feedback message
349    pub fn set_message(&mut self, msg: impl Into<String>) {
350        self.message = Some(msg.into());
351    }
352
353    /// Clear the feedback message
354    pub fn clear_message(&mut self) {
355        self.message = None;
356    }
357
358    /// Set the current overlay
359    pub fn set_overlay(&mut self, overlay: DebugOverlay) {
360        self.overlay = Some(overlay);
361    }
362
363    /// Clear the current overlay
364    pub fn clear_overlay(&mut self) {
365        self.overlay = None;
366    }
367
368    /// Toggle mouse capture mode
369    pub fn toggle_mouse_capture(&mut self) {
370        self.mouse_capture_enabled = !self.mouse_capture_enabled;
371    }
372}
373
374impl<S, A> DebugAdapter<S, A> for DebugLayer<A>
375where
376    S: DebugState + 'static,
377    A: Action + ActionParams,
378{
379    fn render(
380        &mut self,
381        frame: &mut ratatui::Frame,
382        state: &S,
383        render_ctx: RenderContext,
384        render_fn: &mut dyn FnMut(&mut ratatui::Frame, ratatui::layout::Rect, &S, RenderContext),
385    ) {
386        self.render_state(frame, state, |f, area| {
387            render_fn(f, area, state, render_ctx);
388        });
389    }
390
391    fn handle_event(
392        &mut self,
393        event: &tui_dispatch_core::EventKind,
394        state: &S,
395        action_tx: &tokio::sync::mpsc::UnboundedSender<A>,
396    ) -> Option<bool> {
397        self.handle_event_with_state(event, state)
398            .dispatch_queued(|action| {
399                let _ = action_tx.send(action);
400            })
401    }
402
403    fn log_action(&mut self, action: &A) {
404        DebugLayer::log_action(self, action);
405    }
406
407    fn is_enabled(&self) -> bool {
408        DebugLayer::is_enabled(self)
409    }
410
411    #[cfg(feature = "tasks")]
412    fn with_task_manager(self, tasks: &tui_dispatch_core::tasks::TaskManager<A>) -> Self {
413        DebugLayer::with_task_manager(self, tasks)
414    }
415
416    #[cfg(feature = "subscriptions")]
417    fn with_subscriptions(self, subs: &tui_dispatch_core::subscriptions::Subscriptions<A>) -> Self {
418        DebugLayer::with_subscriptions(self, subs)
419    }
420}
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425
426    #[derive(Debug, Clone)]
427    enum TestAction {
428        Foo,
429        Bar,
430    }
431
432    #[test]
433    fn test_debug_freeze_toggle() {
434        let mut freeze: DebugFreeze<TestAction> = DebugFreeze::new();
435
436        assert!(!freeze.enabled);
437
438        freeze.toggle();
439        assert!(freeze.enabled);
440        assert!(freeze.pending_capture);
441
442        freeze.toggle();
443        assert!(!freeze.enabled);
444        assert!(freeze.snapshot.is_none());
445    }
446
447    #[test]
448    fn test_debug_freeze_queue() {
449        let mut freeze: DebugFreeze<TestAction> = DebugFreeze::new();
450        freeze.enable();
451
452        freeze.queue(TestAction::Foo);
453        freeze.queue(TestAction::Bar);
454
455        assert_eq!(freeze.queued_actions.len(), 2);
456
457        let queued = freeze.take_queued();
458        assert_eq!(queued.len(), 2);
459        assert!(freeze.queued_actions.is_empty());
460    }
461
462    #[test]
463    fn test_debug_freeze_message() {
464        let mut freeze: DebugFreeze<TestAction> = DebugFreeze::new();
465
466        freeze.set_message("Test message");
467        assert_eq!(freeze.message, Some("Test message".to_string()));
468
469        freeze.clear_message();
470        assert!(freeze.message.is_none());
471    }
472
473    #[test]
474    fn test_simple_debug_context_binding_context() {
475        use tui_dispatch_core::keybindings::BindingContext;
476
477        // Test name()
478        assert_eq!(SimpleDebugContext::Normal.name(), "normal");
479        assert_eq!(SimpleDebugContext::Debug.name(), "debug");
480
481        // Test from_name()
482        assert_eq!(
483            SimpleDebugContext::from_name("normal"),
484            Some(SimpleDebugContext::Normal)
485        );
486        assert_eq!(
487            SimpleDebugContext::from_name("debug"),
488            Some(SimpleDebugContext::Debug)
489        );
490        assert_eq!(SimpleDebugContext::from_name("invalid"), None);
491
492        // Test all()
493        let all = SimpleDebugContext::all();
494        assert_eq!(all.len(), 2);
495        assert!(all.contains(&SimpleDebugContext::Normal));
496        assert!(all.contains(&SimpleDebugContext::Debug));
497    }
498
499    #[test]
500    fn test_simple_debug_context_default() {
501        let ctx: SimpleDebugContext = Default::default();
502        assert_eq!(ctx, SimpleDebugContext::Normal);
503    }
504}