tui_dispatch_core/debug/
layer.rs

1//! High-level debug layer for TUI applications
2//!
3//! Provides a self-contained debug overlay with automatic pause/resume of
4//! tasks and subscriptions.
5
6use std::io::Write;
7
8use base64::prelude::*;
9use crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEventKind};
10use ratatui::buffer::Buffer;
11use ratatui::layout::Rect;
12use ratatui::style::Style;
13use ratatui::widgets::{Block, Borders, Clear, Scrollbar, ScrollbarOrientation, ScrollbarState};
14use ratatui::Frame;
15
16use super::action_logger::{ActionLog, ActionLogConfig};
17use super::actions::{DebugAction, DebugSideEffect};
18use super::cell::inspect_cell;
19use super::config::DebugStyle;
20use super::state::DebugState;
21use super::table::{ActionLogOverlay, DebugOverlay, DebugTableBuilder, DebugTableOverlay};
22use super::widgets::{
23    dim_buffer, paint_snapshot, ActionLogWidget, BannerItem, CellPreviewWidget, DebugBanner,
24    DebugTableWidget,
25};
26use super::DebugFreeze;
27#[cfg(feature = "subscriptions")]
28use crate::subscriptions::SubPauseHandle;
29#[cfg(feature = "tasks")]
30use crate::tasks::TaskPauseHandle;
31use crate::Action;
32
33/// Location of the debug banner relative to the app area.
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
35pub enum BannerPosition {
36    Bottom,
37    Top,
38}
39
40impl BannerPosition {
41    /// Toggle between top and bottom.
42    pub fn toggle(self) -> Self {
43        match self {
44            Self::Bottom => Self::Top,
45            Self::Top => Self::Bottom,
46        }
47    }
48
49    fn label(self) -> &'static str {
50        match self {
51            Self::Bottom => "bar:bottom",
52            Self::Top => "bar:top",
53        }
54    }
55}
56
57/// Result of handling a debug event.
58pub struct DebugOutcome<A> {
59    /// Whether the debug layer consumed the event.
60    pub consumed: bool,
61    /// Actions queued while debug was active (e.g., from pause/resume).
62    pub queued_actions: Vec<A>,
63    /// Whether a re-render is needed.
64    pub needs_render: bool,
65}
66
67impl<A> DebugOutcome<A> {
68    fn ignored() -> Self {
69        Self {
70            consumed: false,
71            queued_actions: Vec::new(),
72            needs_render: false,
73        }
74    }
75
76    fn consumed(queued_actions: Vec<A>) -> Self {
77        Self {
78            consumed: true,
79            queued_actions,
80            needs_render: true,
81        }
82    }
83
84    /// Dispatch queued actions if the debug layer consumed the event.
85    ///
86    /// Returns `Some(needs_render)` when consumed, otherwise `None`.
87    pub fn dispatch_queued<F>(self, mut dispatch: F) -> Option<bool>
88    where
89        F: FnMut(A),
90    {
91        if !self.consumed {
92            return None;
93        }
94
95        for action in self.queued_actions {
96            dispatch(action);
97        }
98
99        Some(self.needs_render)
100    }
101}
102
103impl<A> Default for DebugOutcome<A> {
104    fn default() -> Self {
105        Self::ignored()
106    }
107}
108
109/// High-level debug layer with minimal configuration.
110///
111/// Provides automatic freeze/unfreeze with pause/resume of tasks and subscriptions.
112///
113/// # Example
114///
115/// ```ignore
116/// use tui_dispatch::debug::DebugLayer;
117///
118/// // Minimal setup with sensible defaults (F12 toggle key)
119/// let mut debug = DebugLayer::simple()
120///     .with_task_manager(&tasks)
121///     .with_subscriptions(&subs)
122///     .active(args.debug);
123///
124/// // In event loop
125/// if debug.intercepts(&event) {
126///     continue;
127/// }
128///
129/// // In render
130/// debug.render(frame, |f, area| {
131///     app.render(f, area);
132/// });
133///
134/// // Log actions for the action log feature
135/// debug.log_action(&action);
136/// ```
137pub struct DebugLayer<A> {
138    /// Key to toggle debug mode
139    toggle_key: KeyCode,
140    /// Internal freeze state
141    freeze: DebugFreeze<A>,
142    /// Where the debug banner is rendered
143    banner_position: BannerPosition,
144    /// Style configuration
145    style: DebugStyle,
146    /// Whether the debug layer is active (can be disabled for release builds)
147    active: bool,
148    /// Action log for display
149    action_log: ActionLog,
150    /// Cached state snapshot for the state overlay
151    state_snapshot: Option<DebugTableOverlay>,
152    /// Scroll offset for state/inspect table overlays
153    table_scroll_offset: usize,
154    /// Cached page size for table overlay scrolling
155    table_page_size: usize,
156    /// Handle to pause/resume task manager
157    #[cfg(feature = "tasks")]
158    task_handle: Option<TaskPauseHandle<A>>,
159    /// Handle to pause/resume subscriptions
160    #[cfg(feature = "subscriptions")]
161    sub_handle: Option<SubPauseHandle>,
162}
163
164impl<A> std::fmt::Debug for DebugLayer<A> {
165    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166        f.debug_struct("DebugLayer")
167            .field("toggle_key", &self.toggle_key)
168            .field("active", &self.active)
169            .field("enabled", &self.freeze.enabled)
170            .field("has_snapshot", &self.freeze.snapshot.is_some())
171            .field("has_state_snapshot", &self.state_snapshot.is_some())
172            .field("banner_position", &self.banner_position)
173            .field("table_scroll_offset", &self.table_scroll_offset)
174            .field("queued_actions", &self.freeze.queued_actions.len())
175            .finish()
176    }
177}
178
179impl<A: Action> DebugLayer<A> {
180    /// Create a new debug layer with the given toggle key.
181    ///
182    /// # Example
183    ///
184    /// ```ignore
185    /// use crossterm::event::KeyCode;
186    ///
187    /// let debug = DebugLayer::new(KeyCode::F(12));
188    /// ```
189    pub fn new(toggle_key: KeyCode) -> Self {
190        Self {
191            toggle_key,
192            freeze: DebugFreeze::new(),
193            banner_position: BannerPosition::Bottom,
194            style: DebugStyle::default(),
195            active: true,
196            action_log: ActionLog::new(ActionLogConfig::with_capacity(100)),
197            state_snapshot: None,
198            table_scroll_offset: 0,
199            table_page_size: 1,
200            #[cfg(feature = "tasks")]
201            task_handle: None,
202            #[cfg(feature = "subscriptions")]
203            sub_handle: None,
204        }
205    }
206
207    /// Create a debug layer with sensible defaults (F12 toggle key).
208    pub fn simple() -> Self {
209        Self::new(KeyCode::F(12))
210    }
211
212    /// Create a debug layer with a custom toggle key.
213    pub fn simple_with_toggle_key(toggle_key: KeyCode) -> Self {
214        Self::new(toggle_key)
215    }
216
217    /// Set whether the debug layer is active.
218    ///
219    /// When inactive (`false`), all methods become no-ops with zero overhead.
220    pub fn active(mut self, active: bool) -> Self {
221        self.active = active;
222        self
223    }
224
225    /// Set the initial banner position.
226    pub fn with_banner_position(mut self, position: BannerPosition) -> Self {
227        self.banner_position = position;
228        self
229    }
230
231    /// Connect a task manager for automatic pause/resume.
232    ///
233    /// When debug mode is enabled, the task manager will be paused.
234    /// When disabled, queued actions will be returned.
235    #[cfg(feature = "tasks")]
236    pub fn with_task_manager(mut self, tasks: &crate::tasks::TaskManager<A>) -> Self {
237        self.task_handle = Some(tasks.pause_handle());
238        self
239    }
240
241    /// Connect subscriptions for automatic pause/resume.
242    ///
243    /// When debug mode is enabled, subscriptions will be paused.
244    #[cfg(feature = "subscriptions")]
245    pub fn with_subscriptions(mut self, subs: &crate::subscriptions::Subscriptions<A>) -> Self {
246        self.sub_handle = Some(subs.pause_handle());
247        self
248    }
249
250    /// Set the action log capacity.
251    pub fn with_action_log_capacity(mut self, capacity: usize) -> Self {
252        self.action_log = ActionLog::new(ActionLogConfig::with_capacity(capacity));
253        self
254    }
255
256    /// Set custom style.
257    pub fn with_style(mut self, style: DebugStyle) -> Self {
258        self.style = style;
259        self
260    }
261
262    /// Check if the debug layer is active.
263    pub fn is_active(&self) -> bool {
264        self.active
265    }
266
267    /// Check if debug mode is enabled (and layer is active).
268    pub fn is_enabled(&self) -> bool {
269        self.active && self.freeze.enabled
270    }
271
272    /// Toggle debug mode on/off and return any side effects.
273    ///
274    /// Returns `None` when the layer is inactive or no side effects are needed.
275    pub fn toggle_enabled(&mut self) -> Option<DebugSideEffect<A>> {
276        if !self.active {
277            return None;
278        }
279        self.toggle()
280    }
281
282    /// Set debug mode on/off and return any side effects.
283    ///
284    /// Returns `None` when the layer is inactive or already in the requested state.
285    pub fn set_enabled(&mut self, enabled: bool) -> Option<DebugSideEffect<A>> {
286        if !self.active || enabled == self.freeze.enabled {
287            return None;
288        }
289        self.toggle()
290    }
291
292    /// Update the banner position (top/bottom) and request a new capture.
293    pub fn set_banner_position(&mut self, position: BannerPosition) {
294        if self.banner_position != position {
295            self.banner_position = position;
296            if self.freeze.enabled {
297                self.freeze.request_capture();
298            }
299        }
300    }
301
302    /// Check if the state overlay is currently visible.
303    pub fn is_state_overlay_visible(&self) -> bool {
304        matches!(self.freeze.overlay, Some(DebugOverlay::State(_)))
305    }
306
307    /// Get a reference to the underlying freeze state.
308    pub fn freeze(&self) -> &DebugFreeze<A> {
309        &self.freeze
310    }
311
312    /// Get a mutable reference to the underlying freeze state.
313    pub fn freeze_mut(&mut self) -> &mut DebugFreeze<A> {
314        &mut self.freeze
315    }
316
317    /// Log an action to the action log.
318    ///
319    /// Call this when dispatching actions to record them for the debug overlay.
320    pub fn log_action<T: crate::ActionParams>(&mut self, action: &T) {
321        if self.active {
322            self.action_log.log(action);
323        }
324    }
325
326    /// Get the action log.
327    pub fn action_log(&self) -> &ActionLog {
328        &self.action_log
329    }
330
331    /// Render with automatic debug handling.
332    ///
333    /// When debug mode is disabled, simply calls `render_fn` with the full frame area.
334    /// When enabled, captures/paints the frozen snapshot and renders debug overlay.
335    pub fn render<F>(&mut self, frame: &mut Frame, render_fn: F)
336    where
337        F: FnOnce(&mut Frame, Rect),
338    {
339        self.render_with_state(frame, |frame, area, _wants_state| {
340            render_fn(frame, area);
341            None
342        });
343    }
344
345    /// Render with optional state capture for the state overlay.
346    ///
347    /// `render_fn` receives the frame, app area, and a `wants_state` hint that
348    /// is `true` when debug mode is active and state data may be requested.
349    /// Return `Some(DebugTableOverlay)` to update the cached state overlay.
350    pub fn render_with_state<F>(&mut self, frame: &mut Frame, render_fn: F)
351    where
352        F: FnOnce(&mut Frame, Rect, bool) -> Option<DebugTableOverlay>,
353    {
354        let screen = frame.area();
355
356        // Inactive or not in debug mode: just render normally
357        if !self.active || !self.freeze.enabled {
358            let _ = render_fn(frame, screen, false);
359            return;
360        }
361
362        // Debug mode: reserve line for banner
363        let (app_area, banner_area) = self.split_for_banner(screen);
364
365        if self.freeze.pending_capture || self.freeze.snapshot.is_none() {
366            // Capture mode: render app, then capture
367            let state_snapshot = render_fn(frame, app_area, true);
368            self.state_snapshot = state_snapshot;
369            if let Some(ref table) = self.state_snapshot {
370                if self.is_state_overlay_visible() {
371                    self.set_state_overlay(table.clone());
372                }
373            }
374            let buffer_clone = frame.buffer_mut().clone();
375            self.freeze.capture(&buffer_clone);
376        } else if let Some(ref snapshot) = self.freeze.snapshot {
377            // Frozen: paint snapshot
378            paint_snapshot(frame, snapshot);
379        }
380
381        // Render debug overlay
382        self.render_debug_overlay(frame, app_area, banner_area);
383    }
384
385    /// Render with a DebugState reference and automatic state table generation.
386    ///
387    /// This is a convenience wrapper around `render_with_state`.
388    pub fn render_state<S: DebugState, F>(&mut self, frame: &mut Frame, state: &S, render_fn: F)
389    where
390        F: FnOnce(&mut Frame, Rect),
391    {
392        self.render_with_state(frame, |frame, area, wants_state| {
393            render_fn(frame, area);
394            if wants_state {
395                Some(state.build_debug_table("Application State"))
396            } else {
397                None
398            }
399        });
400    }
401
402    /// Split area for manual layout control.
403    pub fn split_area(&self, area: Rect) -> (Rect, Rect) {
404        if !self.freeze.enabled {
405            return (area, Rect::ZERO);
406        }
407        self.split_for_banner(area)
408    }
409
410    /// Check if debug layer intercepts an event.
411    ///
412    /// Call this before your normal event handling. If it returns `true`,
413    /// the event was consumed by the debug layer.
414    ///
415    /// # Example
416    ///
417    /// ```ignore
418    /// if debug.intercepts(&event) {
419    ///     continue;
420    /// }
421    /// // Normal event handling
422    /// ```
423    pub fn intercepts(&mut self, event: &crate::EventKind) -> bool {
424        self.intercepts_with_effects(event).is_some()
425    }
426
427    /// Handle a debug event with a single call and return a summary outcome.
428    pub fn handle_event(&mut self, event: &crate::EventKind) -> DebugOutcome<A> {
429        self.handle_event_internal::<()>(event, None)
430    }
431
432    /// Handle a debug event with access to state (for the state overlay).
433    pub fn handle_event_with_state<S: DebugState>(
434        &mut self,
435        event: &crate::EventKind,
436        state: &S,
437    ) -> DebugOutcome<A> {
438        self.handle_event_internal(event, Some(state))
439    }
440
441    /// Check if debug layer intercepts an event, returning any side effects.
442    ///
443    /// Returns `None` if the event was not consumed, `Some(effects)` if it was.
444    pub fn intercepts_with_effects(
445        &mut self,
446        event: &crate::EventKind,
447    ) -> Option<Vec<DebugSideEffect<A>>> {
448        self.intercepts_with_effects_internal::<()>(event, None)
449    }
450
451    /// Check if debug layer intercepts an event with access to app state.
452    ///
453    /// Use this to populate the state overlay when `S` is pressed.
454    pub fn intercepts_with_effects_and_state<S: DebugState>(
455        &mut self,
456        event: &crate::EventKind,
457        state: &S,
458    ) -> Option<Vec<DebugSideEffect<A>>> {
459        self.intercepts_with_effects_internal(event, Some(state))
460    }
461
462    /// Check if debug layer intercepts an event with access to app state.
463    pub fn intercepts_with_state<S: DebugState>(
464        &mut self,
465        event: &crate::EventKind,
466        state: &S,
467    ) -> bool {
468        self.intercepts_with_effects_internal(event, Some(state))
469            .is_some()
470    }
471
472    fn handle_event_internal<S: DebugState>(
473        &mut self,
474        event: &crate::EventKind,
475        state: Option<&S>,
476    ) -> DebugOutcome<A> {
477        let effects = self.intercepts_with_effects_internal(event, state);
478        let Some(effects) = effects else {
479            return DebugOutcome::ignored();
480        };
481
482        let mut queued_actions = Vec::new();
483        for effect in effects {
484            if let DebugSideEffect::ProcessQueuedActions(actions) = effect {
485                queued_actions.extend(actions);
486            }
487        }
488
489        DebugOutcome::consumed(queued_actions)
490    }
491
492    fn intercepts_with_effects_internal<S: DebugState>(
493        &mut self,
494        event: &crate::EventKind,
495        state: Option<&S>,
496    ) -> Option<Vec<DebugSideEffect<A>>> {
497        if !self.active {
498            return None;
499        }
500
501        use crate::EventKind;
502
503        match event {
504            EventKind::Key(key) => self.handle_key_event(*key, state),
505            EventKind::Mouse(mouse) => {
506                if !self.freeze.enabled {
507                    return None;
508                }
509
510                // Only capture mouse when mouse_capture_enabled (toggle with 'i')
511                // When disabled, let terminal handle mouse (allows text selection)
512                if !self.freeze.mouse_capture_enabled {
513                    return None;
514                }
515
516                // Handle click for cell inspection
517                if matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) {
518                    let effect = self.handle_action(DebugAction::InspectCell {
519                        column: mouse.column,
520                        row: mouse.row,
521                    });
522                    return Some(effect.into_iter().collect());
523                }
524
525                // Consume mouse events when capturing
526                Some(vec![])
527            }
528            EventKind::Scroll { delta, .. } => {
529                if !self.freeze.enabled {
530                    return None;
531                }
532
533                match self.freeze.overlay.as_ref() {
534                    Some(DebugOverlay::ActionLog(_)) => {
535                        let action = if *delta > 0 {
536                            DebugAction::ActionLogScrollUp
537                        } else {
538                            DebugAction::ActionLogScrollDown
539                        };
540                        self.handle_action(action);
541                    }
542                    Some(DebugOverlay::State(table)) | Some(DebugOverlay::Inspect(table)) => {
543                        if *delta > 0 {
544                            self.scroll_table_up();
545                        } else {
546                            self.scroll_table_down(table.rows.len());
547                        }
548                    }
549                    _ => {}
550                }
551
552                Some(vec![])
553            }
554            // Don't intercept resize or tick events
555            EventKind::Resize(_, _) | EventKind::Tick => None,
556        }
557    }
558
559    /// Show state overlay using a DebugState implementor.
560    pub fn show_state_overlay<S: DebugState>(&mut self, state: &S) {
561        let table = state.build_debug_table("Application State");
562        self.set_state_overlay(table);
563    }
564
565    /// Show action log overlay.
566    pub fn show_action_log(&mut self) {
567        let overlay = ActionLogOverlay::from_log(&self.action_log, "Action Log");
568        self.freeze.set_overlay(DebugOverlay::ActionLog(overlay));
569    }
570
571    /// Queue an action to be processed when debug mode is disabled.
572    pub fn queue_action(&mut self, action: A) {
573        self.freeze.queue(action);
574    }
575
576    /// Take any queued actions (from task manager resume).
577    ///
578    /// Call this after `intercepts()` returns effects to get queued actions
579    /// that should be dispatched.
580    pub fn take_queued_actions(&mut self) -> Vec<A> {
581        std::mem::take(&mut self.freeze.queued_actions)
582    }
583
584    // =========================================================================
585    // Private helpers
586    // =========================================================================
587
588    fn set_state_overlay(&mut self, table: DebugTableOverlay) {
589        if !matches!(self.freeze.overlay, Some(DebugOverlay::State(_))) {
590            self.table_scroll_offset = 0;
591        }
592        self.state_snapshot = Some(table.clone());
593        self.freeze.set_overlay(DebugOverlay::State(table));
594    }
595
596    fn update_table_scroll(&mut self, table: &DebugTableOverlay, table_area: Rect) {
597        let visible_rows = table_area.height.saturating_sub(1) as usize;
598        self.table_page_size = visible_rows.max(1);
599        let max_offset = table.rows.len().saturating_sub(visible_rows);
600        self.table_scroll_offset = self.table_scroll_offset.min(max_offset);
601    }
602
603    fn build_scrollbar(&self, orientation: ScrollbarOrientation) -> Scrollbar<'static> {
604        let mut scrollbar = Scrollbar::new(orientation)
605            .thumb_style(self.style.scrollbar.thumb)
606            .track_style(self.style.scrollbar.track)
607            .begin_style(self.style.scrollbar.begin)
608            .end_style(self.style.scrollbar.end);
609
610        if let Some(symbol) = self.style.scrollbar.thumb_symbol {
611            scrollbar = scrollbar.thumb_symbol(symbol);
612        }
613        if let Some(symbol) = self.style.scrollbar.track_symbol {
614            scrollbar = scrollbar.track_symbol(Some(symbol));
615        }
616        if let Some(symbol) = self.style.scrollbar.begin_symbol {
617            scrollbar = scrollbar.begin_symbol(Some(symbol));
618        }
619        if let Some(symbol) = self.style.scrollbar.end_symbol {
620            scrollbar = scrollbar.end_symbol(Some(symbol));
621        }
622
623        scrollbar
624    }
625
626    fn table_page_size_value(&self) -> usize {
627        self.table_page_size.max(1)
628    }
629
630    fn table_max_offset(&self, rows_len: usize) -> usize {
631        rows_len.saturating_sub(self.table_page_size_value())
632    }
633
634    fn scroll_table_up(&mut self) {
635        self.table_scroll_offset = self.table_scroll_offset.saturating_sub(1);
636    }
637
638    fn scroll_table_down(&mut self, rows_len: usize) {
639        let max_offset = self.table_max_offset(rows_len);
640        self.table_scroll_offset = (self.table_scroll_offset + 1).min(max_offset);
641    }
642
643    fn scroll_table_to_top(&mut self) {
644        self.table_scroll_offset = 0;
645    }
646
647    fn scroll_table_to_bottom(&mut self, rows_len: usize) {
648        self.table_scroll_offset = self.table_max_offset(rows_len);
649    }
650
651    fn scroll_table_page_up(&mut self) {
652        let page_size = self.table_page_size_value();
653        self.table_scroll_offset = self.table_scroll_offset.saturating_sub(page_size);
654    }
655
656    fn scroll_table_page_down(&mut self, rows_len: usize) {
657        let page_size = self.table_page_size_value();
658        let max_offset = self.table_max_offset(rows_len);
659        self.table_scroll_offset = (self.table_scroll_offset + page_size).min(max_offset);
660    }
661
662    fn handle_table_scroll_key(&mut self, key: KeyCode, rows_len: usize) -> bool {
663        match key {
664            KeyCode::Char('j') | KeyCode::Down => {
665                self.scroll_table_down(rows_len);
666                true
667            }
668            KeyCode::Char('k') | KeyCode::Up => {
669                self.scroll_table_up();
670                true
671            }
672            KeyCode::Char('g') => {
673                self.scroll_table_to_top();
674                true
675            }
676            KeyCode::Char('G') => {
677                self.scroll_table_to_bottom(rows_len);
678                true
679            }
680            KeyCode::PageDown => {
681                self.scroll_table_page_down(rows_len);
682                true
683            }
684            KeyCode::PageUp => {
685                self.scroll_table_page_up();
686                true
687            }
688            _ => false,
689        }
690    }
691
692    fn handle_key_event<S: DebugState>(
693        &mut self,
694        key: KeyEvent,
695        state: Option<&S>,
696    ) -> Option<Vec<DebugSideEffect<A>>> {
697        // Toggle key always works (even when disabled)
698        if key.code == self.toggle_key && key.modifiers.is_empty() {
699            let effect = self.toggle();
700            return Some(effect.into_iter().collect());
701        }
702
703        // Esc also toggles off when enabled
704        if self.freeze.enabled && key.code == KeyCode::Esc {
705            let effect = self.toggle();
706            return Some(effect.into_iter().collect());
707        }
708
709        // Other commands only work when enabled
710        if !self.freeze.enabled {
711            return None;
712        }
713
714        match key.code {
715            KeyCode::Char('b') | KeyCode::Char('B') => {
716                self.banner_position = self.banner_position.toggle();
717                self.freeze.request_capture();
718                return Some(vec![]);
719            }
720            KeyCode::Char('s') | KeyCode::Char('S') => {
721                if matches!(self.freeze.overlay, Some(DebugOverlay::State(_))) {
722                    self.freeze.clear_overlay();
723                } else if let Some(state) = state {
724                    let table = state.build_debug_table("Application State");
725                    self.set_state_overlay(table);
726                } else if let Some(ref table) = self.state_snapshot {
727                    self.set_state_overlay(table.clone());
728                } else {
729                    let table = DebugTableBuilder::new()
730                        .section("State")
731                        .entry(
732                            "hint",
733                            "Press 's' after providing state via render_with_state() or show_state_overlay()",
734                        )
735                        .finish("Application State");
736                    self.freeze.set_overlay(DebugOverlay::State(table));
737                }
738                return Some(vec![]);
739            }
740            _ => {}
741        }
742
743        // Handle internal debug commands (hardcoded keys)
744        let action = match key.code {
745            KeyCode::Char('a') | KeyCode::Char('A') => Some(DebugAction::ToggleActionLog),
746            KeyCode::Char('y') | KeyCode::Char('Y') => Some(DebugAction::CopyFrame),
747            KeyCode::Char('i') | KeyCode::Char('I') => Some(DebugAction::ToggleMouseCapture),
748            KeyCode::Char('q') | KeyCode::Char('Q') => Some(DebugAction::CloseOverlay),
749            _ => None,
750        };
751
752        if let Some(action) = action {
753            let effect = self.handle_action(action);
754            return Some(effect.into_iter().collect());
755        }
756
757        // Handle overlay-specific navigation
758        match &self.freeze.overlay {
759            Some(DebugOverlay::ActionLog(_)) => {
760                let action = match key.code {
761                    KeyCode::Char('j') | KeyCode::Down => Some(DebugAction::ActionLogScrollDown),
762                    KeyCode::Char('k') | KeyCode::Up => Some(DebugAction::ActionLogScrollUp),
763                    KeyCode::Char('g') => Some(DebugAction::ActionLogScrollTop),
764                    KeyCode::Char('G') => Some(DebugAction::ActionLogScrollBottom),
765                    KeyCode::PageDown => Some(DebugAction::ActionLogPageDown),
766                    KeyCode::PageUp => Some(DebugAction::ActionLogPageUp),
767                    KeyCode::Enter => Some(DebugAction::ActionLogShowDetail),
768                    _ => None,
769                };
770                if let Some(action) = action {
771                    self.handle_action(action);
772                    return Some(vec![]);
773                }
774            }
775            Some(DebugOverlay::State(table)) | Some(DebugOverlay::Inspect(table)) => {
776                if self.handle_table_scroll_key(key.code, table.rows.len()) {
777                    return Some(vec![]);
778                }
779            }
780            Some(DebugOverlay::ActionDetail(_)) => {
781                // Back to action log on Esc, Backspace, or Enter
782                if matches!(key.code, KeyCode::Esc | KeyCode::Backspace | KeyCode::Enter) {
783                    self.handle_action(DebugAction::ActionLogBackToList);
784                    return Some(vec![]);
785                }
786            }
787            _ => {}
788        }
789
790        // Consume all key events when debug is enabled
791        Some(vec![])
792    }
793
794    fn toggle(&mut self) -> Option<DebugSideEffect<A>> {
795        if self.freeze.enabled {
796            // Disable: resume tasks/subs
797            #[cfg(feature = "subscriptions")]
798            if let Some(ref handle) = self.sub_handle {
799                handle.resume();
800            }
801
802            #[cfg(feature = "tasks")]
803            let task_queued = if let Some(ref handle) = self.task_handle {
804                handle.resume()
805            } else {
806                vec![]
807            };
808            #[cfg(not(feature = "tasks"))]
809            let task_queued: Vec<A> = vec![];
810
811            let queued = self.freeze.take_queued();
812            self.freeze.disable();
813            self.state_snapshot = None;
814            self.table_scroll_offset = 0;
815            self.table_page_size = 1;
816
817            // Combine queued actions from freeze and task manager
818            let mut all_queued = queued;
819            all_queued.extend(task_queued);
820
821            if all_queued.is_empty() {
822                None
823            } else {
824                Some(DebugSideEffect::ProcessQueuedActions(all_queued))
825            }
826        } else {
827            // Enable: pause tasks/subs
828            #[cfg(feature = "tasks")]
829            if let Some(ref handle) = self.task_handle {
830                handle.pause();
831            }
832            #[cfg(feature = "subscriptions")]
833            if let Some(ref handle) = self.sub_handle {
834                handle.pause();
835            }
836            self.freeze.enable();
837            self.state_snapshot = None;
838            self.table_scroll_offset = 0;
839            self.table_page_size = 1;
840            None
841        }
842    }
843
844    fn handle_action(&mut self, action: DebugAction) -> Option<DebugSideEffect<A>> {
845        match action {
846            DebugAction::Toggle => self.toggle(),
847            DebugAction::CopyFrame => {
848                let text = &self.freeze.snapshot_text;
849                // Use OSC52 escape sequence to copy to clipboard
850                let encoded = BASE64_STANDARD.encode(text);
851                print!("\x1b]52;c;{}\x07", encoded);
852                std::io::stdout().flush().ok();
853                self.freeze.set_message("Copied to clipboard");
854                None
855            }
856            DebugAction::ToggleState => {
857                if matches!(self.freeze.overlay, Some(DebugOverlay::State(_))) {
858                    self.freeze.clear_overlay();
859                } else if let Some(ref table) = self.state_snapshot {
860                    self.set_state_overlay(table.clone());
861                } else {
862                    // Show placeholder - user should call show_state_overlay()
863                    let table = DebugTableBuilder::new()
864                        .section("State")
865                        .entry(
866                            "hint",
867                            "Press 's' after providing state via render_with_state() or show_state_overlay()",
868                        )
869                        .finish("Application State");
870                    self.freeze.set_overlay(DebugOverlay::State(table));
871                }
872                None
873            }
874            DebugAction::ToggleActionLog => {
875                if matches!(self.freeze.overlay, Some(DebugOverlay::ActionLog(_))) {
876                    self.freeze.clear_overlay();
877                } else {
878                    self.show_action_log();
879                }
880                None
881            }
882            DebugAction::ActionLogScrollUp => {
883                if let Some(DebugOverlay::ActionLog(ref mut log)) = self.freeze.overlay {
884                    log.scroll_up();
885                }
886                None
887            }
888            DebugAction::ActionLogScrollDown => {
889                if let Some(DebugOverlay::ActionLog(ref mut log)) = self.freeze.overlay {
890                    log.scroll_down();
891                }
892                None
893            }
894            DebugAction::ActionLogScrollTop => {
895                if let Some(DebugOverlay::ActionLog(ref mut log)) = self.freeze.overlay {
896                    log.scroll_to_top();
897                }
898                None
899            }
900            DebugAction::ActionLogScrollBottom => {
901                if let Some(DebugOverlay::ActionLog(ref mut log)) = self.freeze.overlay {
902                    log.scroll_to_bottom();
903                }
904                None
905            }
906            DebugAction::ActionLogPageUp => {
907                if let Some(DebugOverlay::ActionLog(ref mut log)) = self.freeze.overlay {
908                    log.page_up(10);
909                }
910                None
911            }
912            DebugAction::ActionLogPageDown => {
913                if let Some(DebugOverlay::ActionLog(ref mut log)) = self.freeze.overlay {
914                    log.page_down(10);
915                }
916                None
917            }
918            DebugAction::ActionLogShowDetail => {
919                if let Some(DebugOverlay::ActionLog(ref log)) = self.freeze.overlay {
920                    if let Some(detail) = log.selected_detail() {
921                        self.freeze.set_overlay(DebugOverlay::ActionDetail(detail));
922                    }
923                }
924                None
925            }
926            DebugAction::ActionLogBackToList => {
927                // Go back to action log from detail view
928                if matches!(self.freeze.overlay, Some(DebugOverlay::ActionDetail(_))) {
929                    self.show_action_log();
930                }
931                None
932            }
933            DebugAction::ToggleMouseCapture => {
934                self.freeze.toggle_mouse_capture();
935                None
936            }
937            DebugAction::InspectCell { column, row } => {
938                if let Some(ref snapshot) = self.freeze.snapshot {
939                    let overlay = self.build_inspect_overlay(column, row, snapshot);
940                    self.table_scroll_offset = 0;
941                    self.freeze.set_overlay(DebugOverlay::Inspect(overlay));
942                }
943                self.freeze.mouse_capture_enabled = false;
944                None
945            }
946            DebugAction::CloseOverlay => {
947                self.freeze.clear_overlay();
948                None
949            }
950            DebugAction::RequestCapture => {
951                self.freeze.request_capture();
952                None
953            }
954        }
955    }
956
957    fn split_for_banner(&self, area: Rect) -> (Rect, Rect) {
958        let banner_height = area.height.min(1);
959        match self.banner_position {
960            BannerPosition::Bottom => {
961                let app_area = Rect {
962                    height: area.height.saturating_sub(banner_height),
963                    ..area
964                };
965                let banner_area = Rect {
966                    y: area.y.saturating_add(app_area.height),
967                    height: banner_height,
968                    ..area
969                };
970                (app_area, banner_area)
971            }
972            BannerPosition::Top => {
973                let banner_area = Rect {
974                    y: area.y,
975                    height: banner_height,
976                    ..area
977                };
978                let app_area = Rect {
979                    y: area.y.saturating_add(banner_height),
980                    height: area.height.saturating_sub(banner_height),
981                    ..area
982                };
983                (app_area, banner_area)
984            }
985        }
986    }
987
988    fn render_debug_overlay(&mut self, frame: &mut Frame, app_area: Rect, banner_area: Rect) {
989        let overlay = self.freeze.overlay.clone();
990
991        // Only dim when there's an overlay open
992        if let Some(ref overlay) = overlay {
993            dim_buffer(frame.buffer_mut(), self.style.dim_factor);
994
995            match overlay {
996                DebugOverlay::Inspect(table) | DebugOverlay::State(table) => {
997                    self.render_table_modal(frame, app_area, table);
998                }
999                DebugOverlay::ActionLog(log) => {
1000                    self.render_action_log_modal(frame, app_area, log);
1001                }
1002                DebugOverlay::ActionDetail(detail) => {
1003                    self.render_action_detail_modal(frame, app_area, detail);
1004                }
1005            }
1006        }
1007
1008        // Render banner
1009        self.render_banner(frame, banner_area);
1010    }
1011
1012    fn render_banner(&self, frame: &mut Frame, banner_area: Rect) {
1013        if banner_area.height == 0 {
1014            return;
1015        }
1016
1017        let keys = &self.style.key_styles;
1018        let toggle_key_str = format_key(self.toggle_key);
1019        let mut banner = DebugBanner::new()
1020            .title("DEBUG")
1021            .title_style(self.style.title_style)
1022            .label_style(self.style.label_style)
1023            .background(self.style.banner_bg);
1024
1025        // Add standard debug commands with hardcoded keys
1026        banner = banner.item(BannerItem::new(&toggle_key_str, "resume", keys.toggle));
1027        banner = banner.item(BannerItem::new("a", "actions", keys.actions));
1028        banner = banner.item(BannerItem::new("s", "state", keys.state));
1029        banner = banner.item(BannerItem::new(
1030            "b",
1031            self.banner_position.label(),
1032            keys.actions,
1033        ));
1034        banner = banner.item(BannerItem::new("y", "copy", keys.copy));
1035
1036        if self.freeze.mouse_capture_enabled {
1037            banner = banner.item(BannerItem::new("click", "inspect", keys.mouse));
1038        } else {
1039            banner = banner.item(BannerItem::new("i", "mouse", keys.mouse));
1040        }
1041
1042        // Add message if present
1043        if let Some(ref msg) = self.freeze.message {
1044            banner = banner.item(BannerItem::new("", msg, self.style.value_style));
1045        }
1046
1047        frame.render_widget(banner, banner_area);
1048    }
1049
1050    fn render_table_modal(&mut self, frame: &mut Frame, app_area: Rect, table: &DebugTableOverlay) {
1051        let modal_width = (app_area.width * 80 / 100)
1052            .clamp(30, 120)
1053            .min(app_area.width);
1054        let modal_height = (app_area.height * 60 / 100)
1055            .clamp(8, 40)
1056            .min(app_area.height);
1057
1058        let modal_x = app_area.x + (app_area.width.saturating_sub(modal_width)) / 2;
1059        let modal_y = app_area.y + (app_area.height.saturating_sub(modal_height)) / 2;
1060
1061        let modal_area = Rect::new(modal_x, modal_y, modal_width, modal_height);
1062
1063        frame.render_widget(Clear, modal_area);
1064
1065        let block = Block::default()
1066            .borders(Borders::ALL)
1067            .title(format!(" {} ", table.title))
1068            .style(self.style.banner_bg);
1069
1070        let inner = block.inner(modal_area);
1071        frame.render_widget(block, modal_area);
1072
1073        let mut table_area = if let Some(ref preview) = table.cell_preview {
1074            if inner.height > 3 {
1075                let preview_height = 2u16;
1076                let preview_area = Rect {
1077                    x: inner.x,
1078                    y: inner.y,
1079                    width: inner.width,
1080                    height: 1,
1081                };
1082                let table_area = Rect {
1083                    x: inner.x,
1084                    y: inner.y.saturating_add(preview_height),
1085                    width: inner.width,
1086                    height: inner.height.saturating_sub(preview_height),
1087                };
1088
1089                let preview_widget = CellPreviewWidget::new(preview)
1090                    .label_style(Style::default().fg(DebugStyle::text_secondary()))
1091                    .value_style(Style::default().fg(DebugStyle::text_primary()));
1092                frame.render_widget(preview_widget, preview_area);
1093                table_area
1094            } else {
1095                inner
1096            }
1097        } else {
1098            inner
1099        };
1100
1101        let visible_rows = table_area.height.saturating_sub(1) as usize;
1102        let show_scrollbar =
1103            visible_rows > 0 && table.rows.len() > visible_rows && table_area.width > 11;
1104        let scrollbar_area = if show_scrollbar {
1105            let scrollbar_area = Rect {
1106                x: table_area.x + table_area.width.saturating_sub(1),
1107                width: 1,
1108                ..table_area
1109            };
1110            table_area.width = table_area.width.saturating_sub(1);
1111            Some(Rect {
1112                y: scrollbar_area.y.saturating_add(1),
1113                height: scrollbar_area.height.saturating_sub(1),
1114                ..scrollbar_area
1115            })
1116        } else {
1117            None
1118        };
1119
1120        self.update_table_scroll(table, table_area);
1121        let table_widget = DebugTableWidget::new(table).scroll_offset(self.table_scroll_offset);
1122        frame.render_widget(table_widget, table_area);
1123
1124        if let Some(scrollbar_area) = scrollbar_area {
1125            let content_length = table.rows.len();
1126            let mut scrollbar_state = ScrollbarState::new(content_length)
1127                .position(self.table_scroll_offset)
1128                .viewport_content_length(self.table_page_size_value());
1129            let scrollbar = self.build_scrollbar(ScrollbarOrientation::VerticalRight);
1130            frame.render_stateful_widget(scrollbar, scrollbar_area, &mut scrollbar_state);
1131        }
1132    }
1133
1134    fn render_action_log_modal(&self, frame: &mut Frame, app_area: Rect, log: &ActionLogOverlay) {
1135        let modal_width = (app_area.width * 90 / 100)
1136            .clamp(40, 140)
1137            .min(app_area.width);
1138        let modal_height = (app_area.height * 70 / 100)
1139            .clamp(10, 50)
1140            .min(app_area.height);
1141
1142        let modal_x = app_area.x + (app_area.width.saturating_sub(modal_width)) / 2;
1143        let modal_y = app_area.y + (app_area.height.saturating_sub(modal_height)) / 2;
1144
1145        let modal_area = Rect::new(modal_x, modal_y, modal_width, modal_height);
1146
1147        frame.render_widget(Clear, modal_area);
1148
1149        let entry_count = log.entries.len();
1150        let title = if entry_count > 0 {
1151            format!(" {} ({} entries) ", log.title, entry_count)
1152        } else {
1153            format!(" {} (empty) ", log.title)
1154        };
1155
1156        let block = Block::default()
1157            .borders(Borders::ALL)
1158            .title(title)
1159            .style(self.style.banner_bg);
1160
1161        let mut log_area = block.inner(modal_area);
1162        frame.render_widget(block, modal_area);
1163
1164        let visible_rows = log_area.height.saturating_sub(1) as usize;
1165        let show_scrollbar =
1166            visible_rows > 0 && log.entries.len() > visible_rows && log_area.width > 31;
1167        let scrollbar_area = if show_scrollbar {
1168            let scrollbar_area = Rect {
1169                x: log_area.x + log_area.width.saturating_sub(1),
1170                width: 1,
1171                ..log_area
1172            };
1173            log_area.width = log_area.width.saturating_sub(1);
1174            Some(Rect {
1175                y: scrollbar_area.y.saturating_add(1),
1176                height: scrollbar_area.height.saturating_sub(1),
1177                ..scrollbar_area
1178            })
1179        } else {
1180            None
1181        };
1182
1183        let widget = ActionLogWidget::new(log);
1184        frame.render_widget(widget, log_area);
1185
1186        if let Some(scrollbar_area) = scrollbar_area {
1187            let visible_rows = log_area.height.saturating_sub(1) as usize;
1188            let scroll_offset = log.scroll_offset_for(visible_rows);
1189            let content_length = log.entries.len();
1190            let mut scrollbar_state = ScrollbarState::new(content_length)
1191                .position(scroll_offset)
1192                .viewport_content_length(visible_rows);
1193            let scrollbar = self.build_scrollbar(ScrollbarOrientation::VerticalRight);
1194            frame.render_stateful_widget(scrollbar, scrollbar_area, &mut scrollbar_state);
1195        }
1196    }
1197
1198    fn render_action_detail_modal(
1199        &self,
1200        frame: &mut Frame,
1201        app_area: Rect,
1202        detail: &super::table::ActionDetailOverlay,
1203    ) {
1204        let modal_width = (app_area.width * 80 / 100)
1205            .clamp(40, 120)
1206            .min(app_area.width);
1207        let modal_height = (app_area.height * 50 / 100)
1208            .clamp(8, 30)
1209            .min(app_area.height);
1210
1211        let modal_x = app_area.x + (app_area.width.saturating_sub(modal_width)) / 2;
1212        let modal_y = app_area.y + (app_area.height.saturating_sub(modal_height)) / 2;
1213
1214        let modal_area = Rect::new(modal_x, modal_y, modal_width, modal_height);
1215
1216        frame.render_widget(Clear, modal_area);
1217
1218        let title = format!(" Action #{} - {} ", detail.sequence, detail.name);
1219
1220        let block = Block::default()
1221            .borders(Borders::ALL)
1222            .title(title)
1223            .style(self.style.banner_bg);
1224
1225        let inner = block.inner(modal_area);
1226        frame.render_widget(block, modal_area);
1227
1228        // Build detail content
1229        use ratatui::text::{Line, Span};
1230        use ratatui::widgets::Paragraph;
1231
1232        let label_style = Style::default().fg(DebugStyle::text_secondary());
1233        let value_style = Style::default().fg(DebugStyle::text_primary());
1234
1235        let mut lines = vec![
1236            // Name
1237            Line::from(vec![
1238                Span::styled("Name: ", label_style),
1239                Span::styled(&detail.name, value_style),
1240            ]),
1241            // Sequence
1242            Line::from(vec![
1243                Span::styled("Sequence: ", label_style),
1244                Span::styled(detail.sequence.to_string(), value_style),
1245            ]),
1246            // Elapsed
1247            Line::from(vec![
1248                Span::styled("Elapsed: ", label_style),
1249                Span::styled(&detail.elapsed, value_style),
1250            ]),
1251            // Empty line before params
1252            Line::from(""),
1253            // Parameters header
1254            Line::from(Span::styled("Parameters:", label_style)),
1255        ];
1256
1257        // Parameters content (potentially multi-line)
1258        if detail.params.is_empty() {
1259            lines.push(Line::from(Span::styled("  (none)", value_style)));
1260        } else {
1261            for param_line in detail.params.lines() {
1262                lines.push(Line::from(Span::styled(
1263                    format!("  {}", param_line),
1264                    value_style,
1265                )));
1266            }
1267        }
1268
1269        // Footer hint
1270        lines.push(Line::from(""));
1271        lines.push(Line::from(Span::styled(
1272            "Press Enter/Esc/Backspace to go back",
1273            label_style,
1274        )));
1275
1276        let paragraph = Paragraph::new(lines);
1277        frame.render_widget(paragraph, inner);
1278    }
1279
1280    fn build_inspect_overlay(&self, column: u16, row: u16, snapshot: &Buffer) -> DebugTableOverlay {
1281        let mut builder = DebugTableBuilder::new();
1282
1283        builder.push_section("Position");
1284        builder.push_entry("column", column.to_string());
1285        builder.push_entry("row", row.to_string());
1286
1287        if let Some(preview) = inspect_cell(snapshot, column, row) {
1288            builder.set_cell_preview(preview);
1289        }
1290
1291        builder.finish(format!("Inspect ({column}, {row})"))
1292    }
1293}
1294
1295/// Format a KeyCode for display in the banner.
1296fn format_key(key: KeyCode) -> String {
1297    match key {
1298        KeyCode::F(n) => format!("F{}", n),
1299        KeyCode::Char(c) => c.to_string(),
1300        KeyCode::Esc => "Esc".to_string(),
1301        KeyCode::Enter => "Enter".to_string(),
1302        KeyCode::Tab => "Tab".to_string(),
1303        KeyCode::Backspace => "Bksp".to_string(),
1304        KeyCode::Delete => "Del".to_string(),
1305        KeyCode::Up => "↑".to_string(),
1306        KeyCode::Down => "↓".to_string(),
1307        KeyCode::Left => "←".to_string(),
1308        KeyCode::Right => "→".to_string(),
1309        _ => format!("{:?}", key),
1310    }
1311}
1312
1313#[cfg(test)]
1314mod tests {
1315    use super::*;
1316
1317    #[derive(Debug, Clone)]
1318    enum TestAction {
1319        Foo,
1320        Bar,
1321    }
1322
1323    impl crate::Action for TestAction {
1324        fn name(&self) -> &'static str {
1325            match self {
1326                TestAction::Foo => "Foo",
1327                TestAction::Bar => "Bar",
1328            }
1329        }
1330    }
1331
1332    impl crate::ActionParams for TestAction {
1333        fn params(&self) -> String {
1334            String::new()
1335        }
1336    }
1337
1338    #[test]
1339    fn test_debug_layer_creation() {
1340        let layer: DebugLayer<TestAction> = DebugLayer::new(KeyCode::F(12));
1341        assert!(!layer.is_enabled());
1342        assert!(layer.freeze().snapshot.is_none());
1343    }
1344
1345    #[test]
1346    fn test_toggle() {
1347        let mut layer: DebugLayer<TestAction> = DebugLayer::new(KeyCode::F(12));
1348
1349        // Enable
1350        let effect = layer.toggle();
1351        assert!(effect.is_none());
1352        assert!(layer.is_enabled());
1353
1354        // Disable
1355        let effect = layer.toggle();
1356        assert!(effect.is_none()); // No queued actions
1357        assert!(!layer.is_enabled());
1358    }
1359
1360    #[test]
1361    fn test_set_enabled() {
1362        let mut layer: DebugLayer<TestAction> = DebugLayer::new(KeyCode::F(12));
1363
1364        layer.set_enabled(true);
1365        assert!(layer.is_enabled());
1366
1367        layer.set_enabled(false);
1368        assert!(!layer.is_enabled());
1369    }
1370
1371    #[test]
1372    fn test_simple_constructor() {
1373        let layer: DebugLayer<TestAction> = DebugLayer::simple();
1374        assert!(!layer.is_enabled());
1375    }
1376
1377    #[test]
1378    fn test_queued_actions_returned_on_disable() {
1379        let mut layer: DebugLayer<TestAction> = DebugLayer::new(KeyCode::F(12));
1380
1381        layer.toggle(); // Enable
1382        layer.queue_action(TestAction::Foo);
1383        layer.queue_action(TestAction::Bar);
1384
1385        let effect = layer.toggle(); // Disable
1386
1387        match effect {
1388            Some(DebugSideEffect::ProcessQueuedActions(actions)) => {
1389                assert_eq!(actions.len(), 2);
1390            }
1391            _ => panic!("Expected ProcessQueuedActions"),
1392        }
1393    }
1394
1395    #[test]
1396    fn test_split_area() {
1397        let layer: DebugLayer<TestAction> = DebugLayer::new(KeyCode::F(12));
1398
1399        // Disabled: full area returned
1400        let area = Rect::new(0, 0, 80, 24);
1401        let (app, banner) = layer.split_area(area);
1402        assert_eq!(app, area);
1403        assert_eq!(banner, Rect::ZERO);
1404    }
1405
1406    #[test]
1407    fn test_split_area_enabled() {
1408        let mut layer: DebugLayer<TestAction> = DebugLayer::new(KeyCode::F(12));
1409        layer.toggle();
1410
1411        let area = Rect::new(0, 0, 80, 24);
1412        let (app, banner) = layer.split_area(area);
1413
1414        assert_eq!(app.height, 23);
1415        assert_eq!(banner.height, 1);
1416        assert_eq!(banner.y, 23);
1417    }
1418
1419    #[test]
1420    fn test_split_area_enabled_top() {
1421        let mut layer: DebugLayer<TestAction> = DebugLayer::new(KeyCode::F(12));
1422        layer.toggle();
1423        layer.set_banner_position(BannerPosition::Top);
1424
1425        let area = Rect::new(0, 0, 80, 24);
1426        let (app, banner) = layer.split_area(area);
1427
1428        assert_eq!(banner.y, 0);
1429        assert_eq!(banner.height, 1);
1430        assert_eq!(app.y, 1);
1431        assert_eq!(app.height, 23);
1432    }
1433
1434    #[test]
1435    fn test_inactive_layer() {
1436        let layer: DebugLayer<TestAction> = DebugLayer::new(KeyCode::F(12)).active(false);
1437
1438        assert!(!layer.is_active());
1439        assert!(!layer.is_enabled());
1440    }
1441
1442    #[test]
1443    fn test_action_log() {
1444        let mut layer: DebugLayer<TestAction> = DebugLayer::new(KeyCode::F(12));
1445
1446        layer.log_action(&TestAction::Foo);
1447        layer.log_action(&TestAction::Bar);
1448
1449        assert_eq!(layer.action_log().entries().count(), 2);
1450    }
1451}