tui_dispatch_core/debug/
state.rs

1//! Debug state introspection trait
2//!
3//! Provides a trait for state types to expose their contents for debug overlays.
4
5use super::table::{DebugTableBuilder, DebugTableOverlay};
6
7/// A debug entry (key-value pair)
8#[derive(Debug, Clone)]
9pub struct DebugEntry {
10    pub key: String,
11    pub value: String,
12}
13
14impl DebugEntry {
15    /// Create a new entry
16    pub fn new(key: impl Into<String>, value: impl Into<String>) -> Self {
17        Self {
18            key: key.into(),
19            value: value.into(),
20        }
21    }
22}
23
24/// A debug section with a title and entries
25#[derive(Debug, Clone)]
26pub struct DebugSection {
27    pub title: String,
28    pub entries: Vec<DebugEntry>,
29}
30
31impl DebugSection {
32    /// Create a new section
33    pub fn new(title: impl Into<String>) -> Self {
34        Self {
35            title: title.into(),
36            entries: Vec::new(),
37        }
38    }
39
40    /// Add an entry to the section
41    pub fn entry(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
42        self.entries.push(DebugEntry::new(key, value));
43        self
44    }
45
46    /// Add an entry (mutable)
47    pub fn push_entry(&mut self, key: impl Into<String>, value: impl Into<String>) {
48        self.entries.push(DebugEntry::new(key, value));
49    }
50}
51
52/// Trait for types that can provide debug state information
53///
54/// Implement this trait to enable the state overlay in debug mode.
55///
56/// # Example
57///
58/// ```
59/// use tui_dispatch_core::debug::{DebugState, DebugSection, DebugEntry};
60///
61/// struct AppState {
62///     host: String,
63///     connected: bool,
64///     item_count: usize,
65/// }
66///
67/// impl DebugState for AppState {
68///     fn debug_sections(&self) -> Vec<DebugSection> {
69///         vec![
70///             DebugSection::new("Connection")
71///                 .entry("host", &self.host)
72///                 .entry("connected", self.connected.to_string()),
73///             DebugSection::new("Data")
74///                 .entry("items", self.item_count.to_string()),
75///         ]
76///     }
77/// }
78/// ```
79pub trait DebugState {
80    /// Return state as sections with key-value pairs
81    fn debug_sections(&self) -> Vec<DebugSection>;
82
83    /// Build a debug table overlay from the state
84    ///
85    /// Default implementation uses `debug_sections()`.
86    fn build_debug_table(&self, title: impl Into<String>) -> DebugTableOverlay {
87        let mut builder = DebugTableBuilder::new();
88        for section in self.debug_sections() {
89            builder.push_section(&section.title);
90            for entry in section.entries {
91                builder.push_entry(entry.key, entry.value);
92            }
93        }
94        builder.finish(title)
95    }
96}
97
98/// Blanket implementation for types implementing Debug
99///
100/// Provides a basic fallback that renders the Debug output.
101/// Types should implement DebugState directly for better formatting.
102impl<T: std::fmt::Debug> DebugState for DebugWrapper<'_, T> {
103    fn debug_sections(&self) -> Vec<DebugSection> {
104        vec![DebugSection::new("Debug Output").entry("value", format!("{:#?}", self.0))]
105    }
106}
107
108/// Wrapper to use Debug impl as DebugState
109///
110/// Use this when you want the fallback Debug-based rendering:
111///
112/// ```
113/// use tui_dispatch_core::debug::{DebugState, DebugWrapper};
114///
115/// #[derive(Debug)]
116/// struct MyState { x: i32 }
117///
118/// let state = MyState { x: 42 };
119/// let sections = DebugWrapper(&state).debug_sections();
120/// ```
121pub struct DebugWrapper<'a, T>(pub &'a T);
122
123/// Implementation for unit type (no state to show)
124impl DebugState for () {
125    fn debug_sections(&self) -> Vec<DebugSection> {
126        vec![]
127    }
128}
129
130/// Implementation for tuples - combine multiple state sources
131impl<A: DebugState, B: DebugState> DebugState for (A, B) {
132    fn debug_sections(&self) -> Vec<DebugSection> {
133        let mut sections = self.0.debug_sections();
134        sections.extend(self.1.debug_sections());
135        sections
136    }
137}
138
139impl<A: DebugState, B: DebugState, C: DebugState> DebugState for (A, B, C) {
140    fn debug_sections(&self) -> Vec<DebugSection> {
141        let mut sections = self.0.debug_sections();
142        sections.extend(self.1.debug_sections());
143        sections.extend(self.2.debug_sections());
144        sections
145    }
146}
147
148/// Implementation for references
149impl<T: DebugState> DebugState for &T {
150    fn debug_sections(&self) -> Vec<DebugSection> {
151        (*self).debug_sections()
152    }
153}
154
155impl<T: DebugState> DebugState for &mut T {
156    fn debug_sections(&self) -> Vec<DebugSection> {
157        (**self).debug_sections()
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    struct TestState {
166        name: String,
167        count: usize,
168    }
169
170    impl DebugState for TestState {
171        fn debug_sections(&self) -> Vec<DebugSection> {
172            vec![DebugSection::new("Test")
173                .entry("name", &self.name)
174                .entry("count", self.count.to_string())]
175        }
176    }
177
178    #[test]
179    fn test_debug_state_basic() {
180        let state = TestState {
181            name: "test".to_string(),
182            count: 42,
183        };
184
185        let sections = state.debug_sections();
186        assert_eq!(sections.len(), 1);
187        assert_eq!(sections[0].title, "Test");
188        assert_eq!(sections[0].entries.len(), 2);
189        assert_eq!(sections[0].entries[0].key, "name");
190        assert_eq!(sections[0].entries[0].value, "test");
191    }
192
193    #[test]
194    fn test_build_debug_table() {
195        let state = TestState {
196            name: "foo".to_string(),
197            count: 10,
198        };
199
200        let table = state.build_debug_table("State Info");
201        assert_eq!(table.title, "State Info");
202        assert_eq!(table.rows.len(), 3); // 1 section + 2 entries
203    }
204
205    #[test]
206    fn test_tuple_debug_state() {
207        struct StateA;
208        struct StateB;
209
210        impl DebugState for StateA {
211            fn debug_sections(&self) -> Vec<DebugSection> {
212                vec![DebugSection::new("A").entry("from", "A")]
213            }
214        }
215
216        impl DebugState for StateB {
217            fn debug_sections(&self) -> Vec<DebugSection> {
218                vec![DebugSection::new("B").entry("from", "B")]
219            }
220        }
221
222        let combined = (StateA, StateB);
223        let sections = combined.debug_sections();
224        assert_eq!(sections.len(), 2);
225        assert_eq!(sections[0].title, "A");
226        assert_eq!(sections[1].title, "B");
227    }
228
229    #[test]
230    fn test_debug_wrapper() {
231        #[derive(Debug)]
232        #[allow(dead_code)]
233        struct PlainStruct {
234            x: i32,
235        }
236
237        let s = PlainStruct { x: 42 };
238        let sections = DebugWrapper(&s).debug_sections();
239        assert_eq!(sections.len(), 1);
240        assert!(sections[0].entries[0].value.contains("42"));
241    }
242}