tui_dispatch_core/debug/
config.rs

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