Debug Layer
The debug layer provides powerful debugging tools for TUI applications: frame freeze, state inspection, cell inspection, and clipboard export.
Quick Start
The easiest way to add debugging is with DispatchRuntime:
use tui_dispatch::prelude::*;use tui_dispatch::debug::DebugLayer;
let mut bus = EventBus::new();let keybindings = Keybindings::new();
DispatchRuntime::new(AppState::default(), reducer) .with_debug(DebugLayer::simple()) // F12 to toggle .run_with_bus(terminal, &mut bus, &keybindings, render, is_quit) .await?;For manual event loop integration:
use tui_dispatch::debug::DebugLayer;
let mut store = Store::new(AppState::default(), reducer);let mut debug = DebugLayer::<Action>::simple();
// In event loop - handles toggle key, overlays, etc.if let Some(needs_render) = debug .handle_event(&event.kind) .dispatch_queued(|action| store.dispatch(action)){ should_render = needs_render; continue;}
// In render loop:debug.render_state(frame, store.state(), |f, area| { render_your_app(f, area, store.state());});Similar to Redux DevTools, the debug layer lets you inspect state and action history at runtime.
Default keybindings (when debug mode is active):
- Toggle key (e.g.,
F12) - Toggle debug mode S- Show/hide state overlayB- Toggle banner position (top/bottom)A- Show/hide action logJ/K, arrows,PgUp/PgDn,g/G, mouse wheel - Scroll tablesY- Copy frozen frame to clipboardW- Save state snapshot to a RON fileI- Toggle mouse capture for cell inspectionEsc/Q- Close overlay
To save loadable snapshots for --debug-state-in, configure the layer with
DebugLayer::with_state_snapshots::<AppState>().
Custom Toggle Key
use crossterm::event::KeyCode;
// Use F11 instead of F12let debug = DebugLayer::<Action>::simple_with_toggle_key(KeyCode::F(11));
// Use Escape keylet debug = DebugLayer::<Action>::simple_with_toggle_key(KeyCode::Esc);Programmatic Control
use tui_dispatch::debug::{BannerPosition, DebugLayer};
let mut debug = DebugLayer::<Action>::simple() .with_banner_position(BannerPosition::Top);
if let Some(effect) = debug.set_enabled(true) { handle_debug_effect(effect);}State Inspection
Implement DebugState to show state in the debug overlay. The state overlay is rendered as a tree view, so sections can be expanded/collapsed with Left/Right or Enter/Space.
Manual Implementation
use tui_dispatch::debug::{DebugState, DebugSection};
impl DebugState for AppState { fn debug_sections(&self) -> Vec<DebugSection> { vec![ DebugSection::new("Connection") .entry("host", &self.host) .entry("port", self.port.to_string()), DebugSection::new("UI") .entry("scroll", self.scroll_offset.to_string()), ] }}Derive Macro
Use #[derive(DebugState)] for automatic implementation:
use tui_dispatch::DebugState;
#[derive(DebugState)]struct AppState { #[debug(section = "Connection")] host: String, #[debug(section = "Connection")] port: u16,
#[debug(section = "UI")] scroll_offset: usize,
#[debug(skip)] internal_cache: HashMap<String, Data>,}Attributes
| Attribute | Description |
|---|---|
#[debug(section = "Name")] | Group field under a section |
#[debug(skip)] | Exclude field from debug output |
#[debug(label = "Custom Label")] | Custom label instead of field name |
#[debug(debug_fmt)] | Use {:?} format instead of Display |
#[debug(format = "{:#?}")] | Custom format string |
Example with All Attributes
#[derive(DebugState)]struct ComplexState { #[debug(section = "Info", label = "Full Name")] name: String,
#[debug(section = "Info")] count: usize,
#[debug(section = "Status", debug_fmt)] level: ConnectionStatus,
#[debug(skip)] cache: Vec<u8>,}Showing the State Overlay
// Provide state data during render (recommended):debug.render_state(frame, &app_state, |f, area| { render_your_app(f, area, &app_state);});
// Or trigger it manually:debug.show_state_overlay(&app_state);Cell Inspection
When mouse capture is enabled (I key), clicking on any cell shows its styling:
use tui_dispatch::debug::{inspect_cell, DebugTableBuilder};
if let Some(cell) = inspect_cell(&snapshot, x, y) { let overlay = DebugTableBuilder::new() .section("Cell Info") .entry("position", format!("({}, {})", x, y)) .entry("symbol", format!("'{}'", cell.symbol)) .entry("fg", format!("{:?}", cell.fg)) .entry("bg", format!("{:?}", cell.bg)) .cell_preview(cell) .finish_inspect("Cell Inspector"); debug.freeze_mut().set_overlay(overlay);}Full Control (Escape Hatch)
For custom layouts, use the lower-level methods:
// Split area manuallylet (app_area, banner_area) = debug.split_area(frame.area());
// Custom layoutrender_my_ui(frame, app_area);
// Let debug layer render its partsdebug.render_overlay(frame, app_area);debug.render_banner(frame, banner_area);Handling Side Effects
The debug layer can produce side effects (e.g., clipboard copy):
use tui_dispatch::debug::{DebugAction, DebugSideEffect};
if let Some(effect) = debug.handle_action(DebugAction::CopyFrame) { match effect { DebugSideEffect::CopyToClipboard(text) => { // Copy to clipboard via OSC52 or system clipboard } DebugSideEffect::ProcessQueuedActions(actions) => { // Actions queued while frozen } _ => {} }}See Also
- Debug Sessions & Replay - Action recording, replay with async coordination, JSON schema generation
- Middleware - ActionLoggerMiddleware for action filtering and logging