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