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