tui_dispatch_core/debug/
table.rs

1//! Debug table types and builder
2//!
3//! Provides a builder pattern for constructing debug information tables
4//! with sections and key-value entries. Also includes action log overlay
5//! for displaying recent actions.
6
7use super::action_logger::ActionLog;
8use super::cell::CellPreview;
9
10/// A row in a debug table - either a section header or a key-value entry
11#[derive(Debug, Clone)]
12pub enum DebugTableRow {
13    /// Section header (e.g., "Connection", "Keys", "UI")
14    Section(String),
15    /// Key-value entry (e.g., "host" -> "localhost")
16    Entry { key: String, value: String },
17}
18
19/// A debug table overlay with title, rows, and optional cell preview
20#[derive(Debug, Clone)]
21pub struct DebugTableOverlay {
22    /// Title displayed at the top of the overlay
23    pub title: String,
24    /// Table rows (sections and entries)
25    pub rows: Vec<DebugTableRow>,
26    /// Optional cell preview for inspect overlays
27    pub cell_preview: Option<CellPreview>,
28}
29
30impl DebugTableOverlay {
31    /// Create a new overlay with the given title and rows
32    pub fn new(title: impl Into<String>, rows: Vec<DebugTableRow>) -> Self {
33        Self {
34            title: title.into(),
35            rows,
36            cell_preview: None,
37        }
38    }
39
40    /// Create a new overlay with cell preview
41    pub fn with_cell_preview(
42        title: impl Into<String>,
43        rows: Vec<DebugTableRow>,
44        preview: CellPreview,
45    ) -> Self {
46        Self {
47            title: title.into(),
48            rows,
49            cell_preview: Some(preview),
50        }
51    }
52}
53
54/// Type of debug overlay
55#[derive(Debug, Clone)]
56pub enum DebugOverlay {
57    /// Inspect overlay - shows info about a specific position/cell
58    Inspect(DebugTableOverlay),
59    /// State overlay - shows full application state
60    State(DebugTableOverlay),
61    /// Action log overlay - shows recent actions with timestamps
62    ActionLog(ActionLogOverlay),
63    /// Action detail overlay - shows full details of a single action
64    ActionDetail(ActionDetailOverlay),
65}
66
67/// Overlay for displaying detailed action information
68#[derive(Debug, Clone)]
69pub struct ActionDetailOverlay {
70    /// Sequence number
71    pub sequence: u64,
72    /// Action name
73    pub name: String,
74    /// Full action parameters
75    pub params: String,
76    /// Elapsed time display
77    pub elapsed: String,
78}
79
80impl DebugOverlay {
81    /// Get the underlying table from the overlay (for Table/State/Inspect)
82    pub fn table(&self) -> Option<&DebugTableOverlay> {
83        match self {
84            DebugOverlay::Inspect(table) | DebugOverlay::State(table) => Some(table),
85            DebugOverlay::ActionLog(_) | DebugOverlay::ActionDetail(_) => None,
86        }
87    }
88
89    /// Get the action log overlay
90    pub fn action_log(&self) -> Option<&ActionLogOverlay> {
91        match self {
92            DebugOverlay::ActionLog(log) => Some(log),
93            _ => None,
94        }
95    }
96
97    /// Get the action log overlay mutably
98    pub fn action_log_mut(&mut self) -> Option<&mut ActionLogOverlay> {
99        match self {
100            DebugOverlay::ActionLog(log) => Some(log),
101            _ => None,
102        }
103    }
104
105    /// Get the overlay kind as a string
106    pub fn kind(&self) -> &'static str {
107        match self {
108            DebugOverlay::Inspect(_) => "inspect",
109            DebugOverlay::State(_) => "state",
110            DebugOverlay::ActionLog(_) => "action_log",
111            DebugOverlay::ActionDetail(_) => "action_detail",
112        }
113    }
114}
115
116// ============================================================================
117// Action Log Overlay
118// ============================================================================
119
120/// A display-ready action log entry
121#[derive(Debug, Clone)]
122pub struct ActionLogDisplayEntry {
123    /// Sequence number
124    pub sequence: u64,
125    /// Action name
126    pub name: String,
127    /// Action parameters (without the action name)
128    pub params: String,
129    /// Elapsed time display (e.g., "2.3s")
130    pub elapsed: String,
131}
132
133/// Overlay for displaying the action log
134#[derive(Debug, Clone)]
135pub struct ActionLogOverlay {
136    /// Title for the overlay
137    pub title: String,
138    /// Action entries to display
139    pub entries: Vec<ActionLogDisplayEntry>,
140    /// Currently selected entry index (for scrolling)
141    pub selected: usize,
142    /// Scroll offset for visible window
143    pub scroll_offset: usize,
144}
145
146impl ActionLogOverlay {
147    /// Create from an ActionLog reference
148    pub fn from_log(log: &ActionLog, title: impl Into<String>) -> Self {
149        let entries: Vec<_> = log
150            .entries_rev()
151            .map(|e| ActionLogDisplayEntry {
152                sequence: e.sequence,
153                name: e.name.to_string(),
154                params: e.params.clone(),
155                elapsed: e.elapsed.clone(),
156            })
157            .collect();
158
159        Self {
160            title: title.into(),
161            entries,
162            selected: 0,
163            scroll_offset: 0,
164        }
165    }
166
167    /// Scroll up (select previous entry)
168    pub fn scroll_up(&mut self) {
169        if self.selected > 0 {
170            self.selected -= 1;
171        }
172    }
173
174    /// Scroll down (select next entry)
175    pub fn scroll_down(&mut self) {
176        if self.selected + 1 < self.entries.len() {
177            self.selected += 1;
178        }
179    }
180
181    /// Jump to the top
182    pub fn scroll_to_top(&mut self) {
183        self.selected = 0;
184    }
185
186    /// Jump to the bottom
187    pub fn scroll_to_bottom(&mut self) {
188        if !self.entries.is_empty() {
189            self.selected = self.entries.len() - 1;
190        }
191    }
192
193    /// Page up
194    pub fn page_up(&mut self, page_size: usize) {
195        self.selected = self.selected.saturating_sub(page_size);
196    }
197
198    /// Page down
199    pub fn page_down(&mut self, page_size: usize) {
200        self.selected = (self.selected + page_size).min(self.entries.len().saturating_sub(1));
201    }
202
203    /// Get the currently selected entry
204    pub fn get_selected(&self) -> Option<&ActionLogDisplayEntry> {
205        self.entries.get(self.selected)
206    }
207
208    /// Create a detail overlay from the selected entry
209    pub fn selected_detail(&self) -> Option<ActionDetailOverlay> {
210        self.get_selected().map(|entry| ActionDetailOverlay {
211            sequence: entry.sequence,
212            name: entry.name.clone(),
213            params: entry.params.clone(),
214            elapsed: entry.elapsed.clone(),
215        })
216    }
217}
218
219/// Builder for constructing debug tables
220///
221/// # Example
222///
223/// ```
224/// use tui_dispatch_core::debug::{DebugTableBuilder, DebugTableRow};
225///
226/// let table = DebugTableBuilder::new()
227///     .section("Connection")
228///     .entry("host", "localhost")
229///     .entry("port", "6379")
230///     .section("Status")
231///     .entry("connected", "true")
232///     .finish("Connection Info");
233///
234/// assert_eq!(table.title, "Connection Info");
235/// assert_eq!(table.rows.len(), 5);
236/// ```
237#[derive(Debug, Default)]
238pub struct DebugTableBuilder {
239    rows: Vec<DebugTableRow>,
240    cell_preview: Option<CellPreview>,
241}
242
243impl DebugTableBuilder {
244    /// Create a new empty builder
245    pub fn new() -> Self {
246        Self::default()
247    }
248
249    /// Add a section header
250    pub fn section(mut self, title: impl Into<String>) -> Self {
251        self.rows.push(DebugTableRow::Section(title.into()));
252        self
253    }
254
255    /// Add a key-value entry
256    pub fn entry(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
257        self.rows.push(DebugTableRow::Entry {
258            key: key.into(),
259            value: value.into(),
260        });
261        self
262    }
263
264    /// Add a section header (mutable reference version)
265    pub fn push_section(&mut self, title: impl Into<String>) {
266        self.rows.push(DebugTableRow::Section(title.into()));
267    }
268
269    /// Add a key-value entry (mutable reference version)
270    pub fn push_entry(&mut self, key: impl Into<String>, value: impl Into<String>) {
271        self.rows.push(DebugTableRow::Entry {
272            key: key.into(),
273            value: value.into(),
274        });
275    }
276
277    /// Set the cell preview for inspect overlays
278    pub fn cell_preview(mut self, preview: CellPreview) -> Self {
279        self.cell_preview = Some(preview);
280        self
281    }
282
283    /// Set the cell preview (mutable reference version)
284    pub fn set_cell_preview(&mut self, preview: CellPreview) {
285        self.cell_preview = Some(preview);
286    }
287
288    /// Build the final table overlay with the given title
289    pub fn finish(self, title: impl Into<String>) -> DebugTableOverlay {
290        DebugTableOverlay {
291            title: title.into(),
292            rows: self.rows,
293            cell_preview: self.cell_preview,
294        }
295    }
296
297    /// Build as an inspect overlay
298    pub fn finish_inspect(self, title: impl Into<String>) -> DebugOverlay {
299        DebugOverlay::Inspect(self.finish(title))
300    }
301
302    /// Build as a state overlay
303    pub fn finish_state(self, title: impl Into<String>) -> DebugOverlay {
304        DebugOverlay::State(self.finish(title))
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311
312    #[test]
313    fn test_builder_basic() {
314        let table = DebugTableBuilder::new()
315            .section("Test")
316            .entry("key1", "value1")
317            .entry("key2", "value2")
318            .finish("Test Table");
319
320        assert_eq!(table.title, "Test Table");
321        assert_eq!(table.rows.len(), 3);
322        assert!(table.cell_preview.is_none());
323    }
324
325    #[test]
326    fn test_builder_multiple_sections() {
327        let table = DebugTableBuilder::new()
328            .section("Section 1")
329            .entry("a", "1")
330            .section("Section 2")
331            .entry("b", "2")
332            .finish("Multi-Section");
333
334        assert_eq!(table.rows.len(), 4);
335
336        match &table.rows[0] {
337            DebugTableRow::Section(s) => assert_eq!(s, "Section 1"),
338            _ => panic!("Expected section"),
339        }
340        match &table.rows[2] {
341            DebugTableRow::Section(s) => assert_eq!(s, "Section 2"),
342            _ => panic!("Expected section"),
343        }
344    }
345
346    #[test]
347    fn test_overlay_kinds() {
348        let table = DebugTableBuilder::new().finish("Test");
349
350        let inspect = DebugOverlay::Inspect(table.clone());
351        assert_eq!(inspect.kind(), "inspect");
352        assert!(inspect.table().is_some());
353        assert!(inspect.action_log().is_none());
354
355        let state = DebugOverlay::State(table);
356        assert_eq!(state.kind(), "state");
357
358        let action_log = ActionLogOverlay {
359            title: "Test".to_string(),
360            entries: vec![],
361            selected: 0,
362            scroll_offset: 0,
363        };
364        let log_overlay = DebugOverlay::ActionLog(action_log);
365        assert_eq!(log_overlay.kind(), "action_log");
366        assert!(log_overlay.table().is_none());
367        assert!(log_overlay.action_log().is_some());
368    }
369
370    #[test]
371    fn test_action_log_overlay_scrolling() {
372        let mut overlay = ActionLogOverlay {
373            title: "Test".to_string(),
374            entries: vec![
375                ActionLogDisplayEntry {
376                    sequence: 0,
377                    name: "A".to_string(),
378                    params: "".to_string(),
379                    elapsed: "0ms".to_string(),
380                },
381                ActionLogDisplayEntry {
382                    sequence: 1,
383                    name: "B".to_string(),
384                    params: "x: 1".to_string(),
385                    elapsed: "1ms".to_string(),
386                },
387                ActionLogDisplayEntry {
388                    sequence: 2,
389                    name: "C".to_string(),
390                    params: "y: 2".to_string(),
391                    elapsed: "2ms".to_string(),
392                },
393            ],
394            selected: 0,
395            scroll_offset: 0,
396        };
397
398        assert_eq!(overlay.selected, 0);
399
400        overlay.scroll_down();
401        assert_eq!(overlay.selected, 1);
402
403        overlay.scroll_down();
404        assert_eq!(overlay.selected, 2);
405
406        overlay.scroll_down(); // Should not go past end
407        assert_eq!(overlay.selected, 2);
408
409        overlay.scroll_up();
410        assert_eq!(overlay.selected, 1);
411
412        overlay.scroll_to_top();
413        assert_eq!(overlay.selected, 0);
414
415        overlay.scroll_to_bottom();
416        assert_eq!(overlay.selected, 2);
417    }
418}