Skip to main content

tui_dispatch_debug/debug/
config.rs

1//! Debug layer configuration
2
3use super::SimpleDebugContext;
4use ratatui::style::{Color, Modifier, Style};
5use tui_dispatch_core::keybindings::{BindingContext, Keybindings};
6
7// Neon color palette (matches memtui theme)
8const NEON_PURPLE: Color = Color::Rgb(160, 100, 220);
9const NEON_PINK: Color = Color::Rgb(255, 100, 150);
10const NEON_AMBER: Color = Color::Rgb(255, 191, 0);
11const NEON_CYAN: Color = Color::Rgb(0, 255, 255);
12const NEON_GREEN: Color = Color::Rgb(80, 255, 120);
13const ELECTRIC_BLUE: Color = Color::Rgb(80, 180, 255);
14const KINDA_GREEN: Color = Color::Rgb(40, 220, 80);
15
16const ACCENT_MINT: Color = Color::Rgb(0x36, 0xE3, 0x95);
17
18const BG_DEEP: Color = Color::Rgb(12, 14, 22);
19const BG_PANEL: Color = Color::Rgb(18, 21, 32);
20const BG_SURFACE: Color = Color::Rgb(26, 30, 44);
21const BG_HIGHLIGHT: Color = Color::Rgb(25, 30, 40);
22
23const OVERLAY_BG: Color = Color::Rgb(46, 46, 58);
24const OVERLAY_BG_ALT: Color = Color::Rgb(36, 36, 48);
25const OVERLAY_BG_DARK: Color = Color::Rgb(28, 28, 40);
26
27const TEXT_PRIMARY: Color = Color::Rgb(240, 240, 245);
28const TEXT_SECONDARY: Color = Color::Rgb(150, 150, 160);
29
30/// Style configuration for debug UI.
31///
32/// Includes both high-level composed styles (banner, keys, scrollbar) and a
33/// color palette that can be overridden to theme the entire debug overlay.
34///
35/// # Example
36///
37/// ```
38/// use ratatui::style::Color;
39/// use tui_dispatch_debug::debug::DebugStyle;
40///
41/// let mut style = DebugStyle::default();
42/// style.accent = Color::Rgb(255, 0, 128);
43/// style.neon_green = Color::Rgb(0, 200, 100);
44/// ```
45#[derive(Debug, Clone)]
46pub struct DebugStyle {
47    /// Background style for the banner
48    pub banner_bg: Style,
49    /// Title style (e.g., "DEBUG" label)
50    pub title_style: Style,
51    /// Key styles for different actions (toggle, state, copy, mouse)
52    pub key_styles: KeyStyles,
53    /// Scrollbar styling for debug overlays
54    pub scrollbar: ScrollbarStyle,
55    /// Label style (e.g., "resume")
56    pub label_style: Style,
57    /// Value style for status items
58    pub value_style: Style,
59    /// Dim factor for background (0.0-1.0)
60    pub dim_factor: f32,
61
62    // -- Color palette --
63    /// Accent color used for titles, headers, and highlights
64    pub accent: Color,
65    /// Primary text color
66    pub text_primary: Color,
67    /// Secondary/muted text color
68    pub text_secondary: Color,
69    /// Deep background color
70    pub bg_deep: Color,
71    /// Surface background color
72    pub bg_surface: Color,
73    /// Highlight background (selected items)
74    pub bg_highlight: Color,
75    /// Overlay background
76    pub overlay_bg: Color,
77    /// Alternate overlay background (for striped rows)
78    pub overlay_bg_alt: Color,
79    /// Darker overlay background (headers, footers)
80    pub overlay_bg_dark: Color,
81    /// Purple accent for types, keywords, section headers
82    pub neon_purple: Color,
83    /// Cyan accent for type names, links
84    pub neon_cyan: Color,
85    /// Amber accent for keys, field names
86    pub neon_amber: Color,
87    /// Green accent for strings, IDs
88    pub neon_green: Color,
89}
90
91/// Style and symbol overrides for debug scrollbars
92#[derive(Debug, Clone)]
93pub struct ScrollbarStyle {
94    /// Style for the scrollbar thumb
95    pub thumb: Style,
96    /// Style for the scrollbar track
97    pub track: Style,
98    /// Style for the begin symbol
99    pub begin: Style,
100    /// Style for the end symbol
101    pub end: Style,
102    /// Override for the thumb symbol
103    pub thumb_symbol: Option<&'static str>,
104    /// Override for the track symbol
105    pub track_symbol: Option<&'static str>,
106    /// Override for the begin symbol
107    pub begin_symbol: Option<&'static str>,
108    /// Override for the end symbol
109    pub end_symbol: Option<&'static str>,
110}
111
112impl Default for ScrollbarStyle {
113    fn default() -> Self {
114        Self {
115            thumb: Style::default().fg(ACCENT_MINT),
116            track: Style::default().fg(ACCENT_MINT),
117            begin: Style::default().fg(ACCENT_MINT),
118            end: Style::default().fg(ACCENT_MINT),
119            thumb_symbol: Some("█"),
120            track_symbol: Some("│"),
121            begin_symbol: None,
122            end_symbol: None,
123        }
124    }
125}
126
127/// Styles for different debug key hints
128#[derive(Debug, Clone)]
129pub struct KeyStyles {
130    /// Style for toggle key (F12)
131    pub toggle: Style,
132    /// Style for state key (S)
133    pub state: Style,
134    /// Style for copy key (Y)
135    pub copy: Style,
136    /// Style for mouse key (I)
137    pub mouse: Style,
138    /// Style for actions key (A)
139    pub actions: Style,
140    /// Style for components key (C)
141    pub components: Style,
142}
143
144impl Default for KeyStyles {
145    fn default() -> Self {
146        let key_base = |bg: Color| {
147            Style::default()
148                .fg(BG_DEEP)
149                .bg(bg)
150                .add_modifier(Modifier::BOLD)
151        };
152        Self {
153            toggle: key_base(NEON_PINK),
154            state: key_base(NEON_CYAN),
155            copy: key_base(NEON_AMBER),
156            mouse: key_base(ELECTRIC_BLUE),
157            actions: key_base(KINDA_GREEN),
158            components: key_base(NEON_PURPLE),
159        }
160    }
161}
162
163impl Default for DebugStyle {
164    fn default() -> Self {
165        Self {
166            banner_bg: Style::default().bg(BG_DEEP),
167            title_style: Style::default()
168                .fg(BG_DEEP)
169                .bg(NEON_PURPLE)
170                .add_modifier(Modifier::BOLD),
171            key_styles: KeyStyles::default(),
172            scrollbar: ScrollbarStyle::default(),
173            label_style: Style::default().fg(TEXT_SECONDARY),
174            value_style: Style::default().fg(TEXT_PRIMARY),
175            dim_factor: 0.7,
176            accent: ACCENT_MINT,
177            text_primary: TEXT_PRIMARY,
178            text_secondary: TEXT_SECONDARY,
179            bg_deep: BG_DEEP,
180            bg_surface: BG_SURFACE,
181            bg_highlight: BG_HIGHLIGHT,
182            overlay_bg: OVERLAY_BG,
183            overlay_bg_alt: OVERLAY_BG_ALT,
184            overlay_bg_dark: OVERLAY_BG_DARK,
185            neon_purple: NEON_PURPLE,
186            neon_cyan: NEON_CYAN,
187            neon_amber: NEON_AMBER,
188            neon_green: NEON_GREEN,
189        }
190    }
191}
192
193// Deprecated static color accessors — use instance fields instead.
194#[allow(deprecated)]
195impl DebugStyle {
196    #[deprecated(note = "use the `neon_purple` field on a DebugStyle instance")]
197    pub const fn neon_purple() -> Color {
198        NEON_PURPLE
199    }
200    #[deprecated(note = "use the `neon_cyan` field on a DebugStyle instance")]
201    pub const fn neon_cyan() -> Color {
202        NEON_CYAN
203    }
204    #[deprecated(note = "use the `neon_amber` field on a DebugStyle instance")]
205    pub const fn neon_amber() -> Color {
206        NEON_AMBER
207    }
208    #[deprecated(note = "use the `neon_green` field on a DebugStyle instance")]
209    pub const fn neon_green() -> Color {
210        NEON_GREEN
211    }
212    #[deprecated(note = "use the `accent` field on a DebugStyle instance")]
213    pub const fn accent() -> Color {
214        ACCENT_MINT
215    }
216    #[deprecated(note = "use the `bg_deep` field on a DebugStyle instance")]
217    pub const fn bg_deep() -> Color {
218        BG_DEEP
219    }
220    /// Returns the panel background color (not exposed as instance field).
221    pub const fn bg_panel() -> Color {
222        BG_PANEL
223    }
224    #[deprecated(note = "use the `bg_surface` field on a DebugStyle instance")]
225    pub const fn bg_surface() -> Color {
226        BG_SURFACE
227    }
228    #[deprecated(note = "use the `bg_highlight` field on a DebugStyle instance")]
229    pub const fn bg_highlight() -> Color {
230        BG_HIGHLIGHT
231    }
232    #[deprecated(note = "use the `overlay_bg` field on a DebugStyle instance")]
233    pub const fn overlay_bg() -> Color {
234        OVERLAY_BG
235    }
236    #[deprecated(note = "use the `overlay_bg_alt` field on a DebugStyle instance")]
237    pub const fn overlay_bg_alt() -> Color {
238        OVERLAY_BG_ALT
239    }
240    #[deprecated(note = "use the `overlay_bg_dark` field on a DebugStyle instance")]
241    pub const fn overlay_bg_dark() -> Color {
242        OVERLAY_BG_DARK
243    }
244    #[deprecated(note = "use the `text_primary` field on a DebugStyle instance")]
245    pub const fn text_primary() -> Color {
246        TEXT_PRIMARY
247    }
248    #[deprecated(note = "use the `text_secondary` field on a DebugStyle instance")]
249    pub const fn text_secondary() -> Color {
250        TEXT_SECONDARY
251    }
252}
253
254/// Status item for the debug banner
255#[derive(Debug, Clone)]
256pub struct StatusItem {
257    /// Label/key text
258    pub label: String,
259    /// Value text
260    pub value: String,
261    /// Optional custom style
262    pub style: Option<Style>,
263}
264
265impl StatusItem {
266    /// Create a new status item
267    pub fn new(label: impl Into<String>, value: impl Into<String>) -> Self {
268        Self {
269            label: label.into(),
270            value: value.into(),
271            style: None,
272        }
273    }
274
275    /// Set custom style
276    pub fn with_style(mut self, style: Style) -> Self {
277        self.style = Some(style);
278        self
279    }
280}
281
282/// Configuration for the debug layer
283#[derive(Clone)]
284pub struct DebugConfig<C: BindingContext> {
285    /// Keybindings for debug commands
286    pub keybindings: Keybindings<C>,
287    /// Context used for debug-specific bindings
288    pub debug_context: C,
289    /// Style configuration
290    pub style: DebugStyle,
291    /// Status items provider (called each render)
292    status_provider: Option<StatusProvider>,
293}
294
295type StatusProvider = std::sync::Arc<dyn Fn() -> Vec<StatusItem> + Send + Sync>;
296
297impl<C: BindingContext> std::fmt::Debug for DebugConfig<C> {
298    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
299        f.debug_struct("DebugConfig")
300            .field("debug_context", &self.debug_context.name())
301            .field("style", &self.style)
302            .field(
303                "status_provider",
304                &self.status_provider.as_ref().map(|_| "<fn>"),
305            )
306            .finish()
307    }
308}
309
310impl<C: BindingContext> DebugConfig<C> {
311    /// Create a new config with keybindings and debug context
312    pub fn new(keybindings: Keybindings<C>, debug_context: C) -> Self {
313        Self {
314            keybindings,
315            debug_context,
316            style: DebugStyle::default(),
317            status_provider: None,
318        }
319    }
320
321    /// Set the style
322    pub fn with_style(mut self, style: DebugStyle) -> Self {
323        self.style = style;
324        self
325    }
326
327    /// Set a status provider function
328    ///
329    /// This function is called each render to get status items for the banner.
330    pub fn with_status_provider<F>(mut self, provider: F) -> Self
331    where
332        F: Fn() -> Vec<StatusItem> + Send + Sync + 'static,
333    {
334        self.status_provider = Some(std::sync::Arc::new(provider));
335        self
336    }
337
338    /// Get status items from the provider (if any)
339    pub fn status_items(&self) -> Vec<StatusItem> {
340        self.status_provider
341            .as_ref()
342            .map(|f| f())
343            .unwrap_or_default()
344    }
345}
346
347// ============================================================================
348// Default Debug Keybindings
349// ============================================================================
350
351/// Create default debug keybindings with `F12`/`Esc` to toggle.
352///
353/// Default bindings:
354/// - `debug.toggle`: F12, Esc
355/// - `debug.state`: s, S
356/// - `debug.copy`: y, Y
357/// - `debug.mouse`: i, I
358///
359/// # Example
360///
361/// ```
362/// use tui_dispatch_debug::debug::default_debug_keybindings;
363///
364/// let kb = default_debug_keybindings();
365/// ```
366pub fn default_debug_keybindings() -> Keybindings<SimpleDebugContext> {
367    default_debug_keybindings_with_toggle(&["F12", "Esc"])
368}
369
370/// Create debug keybindings with custom toggle key(s).
371///
372/// Same as [`default_debug_keybindings`] but uses the provided key(s)
373/// for toggling debug mode instead of `F12`/`Esc`.
374///
375/// # Example
376///
377/// ```
378/// use tui_dispatch_debug::debug::default_debug_keybindings_with_toggle;
379///
380/// // Use F11 instead of F12
381/// let kb = default_debug_keybindings_with_toggle(&["F11"]);
382///
383/// // Multiple toggle keys
384/// let kb = default_debug_keybindings_with_toggle(&["F11", "Ctrl+D"]);
385/// ```
386pub fn default_debug_keybindings_with_toggle(
387    toggle_keys: &[&str],
388) -> Keybindings<SimpleDebugContext> {
389    let mut kb = Keybindings::new();
390    kb.add(
391        SimpleDebugContext::Debug,
392        "debug.toggle",
393        toggle_keys.iter().map(|s| (*s).into()).collect(),
394    );
395    kb.add(
396        SimpleDebugContext::Debug,
397        "debug.state",
398        vec!["s".into(), "S".into()],
399    );
400    kb.add(
401        SimpleDebugContext::Debug,
402        "debug.copy",
403        vec!["y".into(), "Y".into()],
404    );
405    kb.add(
406        SimpleDebugContext::Debug,
407        "debug.mouse",
408        vec!["i".into(), "I".into()],
409    );
410    kb.add(
411        SimpleDebugContext::Debug,
412        "debug.action_log",
413        vec!["a".into(), "A".into()],
414    );
415    kb.add(
416        SimpleDebugContext::Debug,
417        "debug.components",
418        vec!["c".into(), "C".into()],
419    );
420    kb
421}
422
423#[cfg(test)]
424mod tests {
425    use super::*;
426
427    // Minimal test context
428    #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
429    enum TestContext {
430        Debug,
431    }
432
433    impl BindingContext for TestContext {
434        fn name(&self) -> &'static str {
435            "debug"
436        }
437        fn from_name(name: &str) -> Option<Self> {
438            (name == "debug").then_some(TestContext::Debug)
439        }
440        fn all() -> &'static [Self] {
441            &[TestContext::Debug]
442        }
443    }
444
445    #[test]
446    fn test_status_item() {
447        let item = StatusItem::new("keys", "42");
448        assert_eq!(item.label, "keys");
449        assert_eq!(item.value, "42");
450        assert!(item.style.is_none());
451
452        let styled = item.with_style(Style::default().fg(Color::Red));
453        assert!(styled.style.is_some());
454    }
455
456    #[test]
457    fn test_config_with_status_provider() {
458        let config = DebugConfig::new(Keybindings::new(), TestContext::Debug)
459            .with_status_provider(|| vec![StatusItem::new("test", "value")]);
460
461        let items = config.status_items();
462        assert_eq!(items.len(), 1);
463        assert_eq!(items[0].label, "test");
464    }
465
466    #[test]
467    fn test_config_without_provider() {
468        let config: DebugConfig<TestContext> =
469            DebugConfig::new(Keybindings::new(), TestContext::Debug);
470        let items = config.status_items();
471        assert!(items.is_empty());
472    }
473
474    #[test]
475    fn test_default_debug_keybindings() {
476        let kb = default_debug_keybindings();
477        let bindings = kb.get_context_bindings(SimpleDebugContext::Debug).unwrap();
478
479        // Check all bindings exist
480        assert!(bindings.contains_key("debug.toggle"));
481        assert!(bindings.contains_key("debug.state"));
482        assert!(bindings.contains_key("debug.copy"));
483        assert!(bindings.contains_key("debug.mouse"));
484
485        // Check default toggle keys
486        let toggle = bindings.get("debug.toggle").unwrap();
487        assert!(toggle.contains(&"F12".to_string()));
488        assert!(toggle.contains(&"Esc".to_string()));
489
490        // Check state keys
491        let state = bindings.get("debug.state").unwrap();
492        assert!(state.contains(&"s".to_string()));
493        assert!(state.contains(&"S".to_string()));
494
495        // Check copy keys
496        let copy = bindings.get("debug.copy").unwrap();
497        assert!(copy.contains(&"y".to_string()));
498        assert!(copy.contains(&"Y".to_string()));
499
500        // Check mouse keys
501        let mouse = bindings.get("debug.mouse").unwrap();
502        assert!(mouse.contains(&"i".to_string()));
503        assert!(mouse.contains(&"I".to_string()));
504    }
505
506    #[test]
507    fn test_default_debug_keybindings_with_toggle_custom() {
508        let kb = default_debug_keybindings_with_toggle(&["F11"]);
509        let bindings = kb.get_context_bindings(SimpleDebugContext::Debug).unwrap();
510
511        // Check custom toggle key
512        let toggle = bindings.get("debug.toggle").unwrap();
513        assert!(toggle.contains(&"F11".to_string()));
514        assert!(!toggle.contains(&"F12".to_string())); // Should not have F12
515
516        // Other bindings should still be default
517        let state = bindings.get("debug.state").unwrap();
518        assert!(state.contains(&"s".to_string()));
519    }
520
521    #[test]
522    fn test_default_debug_keybindings_with_toggle_multiple() {
523        let kb = default_debug_keybindings_with_toggle(&["F11", "Ctrl+D"]);
524        let bindings = kb.get_context_bindings(SimpleDebugContext::Debug).unwrap();
525
526        let toggle = bindings.get("debug.toggle").unwrap();
527        assert!(toggle.contains(&"F11".to_string()));
528        assert!(toggle.contains(&"Ctrl+D".to_string()));
529    }
530}