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