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    /// Calculate the scroll offset to keep the selected row visible.
204    pub fn scroll_offset_for(&self, visible_rows: usize) -> usize {
205        if visible_rows == 0 {
206            return 0;
207        }
208        if self.selected >= visible_rows {
209            self.selected - visible_rows + 1
210        } else {
211            0
212        }
213    }
214
215    /// Get the currently selected entry
216    pub fn get_selected(&self) -> Option<&ActionLogDisplayEntry> {
217        self.entries.get(self.selected)
218    }
219
220    /// Create a detail overlay from the selected entry
221    pub fn selected_detail(&self) -> Option<ActionDetailOverlay> {
222        self.get_selected().map(|entry| ActionDetailOverlay {
223            sequence: entry.sequence,
224            name: entry.name.clone(),
225            params: entry.params.clone(),
226            elapsed: entry.elapsed.clone(),
227        })
228    }
229}
230
231/// Builder for constructing debug tables
232///
233/// # Example
234///
235/// ```
236/// use tui_dispatch_core::debug::{DebugTableBuilder, DebugTableRow};
237///
238/// let table = DebugTableBuilder::new()
239///     .section("Connection")
240///     .entry("host", "localhost")
241///     .entry("port", "6379")
242///     .section("Status")
243///     .entry("connected", "true")
244///     .finish("Connection Info");
245///
246/// assert_eq!(table.title, "Connection Info");
247/// assert_eq!(table.rows.len(), 5);
248/// ```
249#[derive(Debug, Default)]
250pub struct DebugTableBuilder {
251    rows: Vec<DebugTableRow>,
252    cell_preview: Option<CellPreview>,
253}
254
255impl DebugTableBuilder {
256    /// Create a new empty builder
257    pub fn new() -> Self {
258        Self::default()
259    }
260
261    /// Add a section header
262    pub fn section(mut self, title: impl Into<String>) -> Self {
263        self.rows.push(DebugTableRow::Section(title.into()));
264        self
265    }
266
267    /// Add a key-value entry
268    pub fn entry(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
269        self.rows.push(DebugTableRow::Entry {
270            key: key.into(),
271            value: value.into(),
272        });
273        self
274    }
275
276    /// Add a section header (mutable reference version)
277    pub fn push_section(&mut self, title: impl Into<String>) {
278        self.rows.push(DebugTableRow::Section(title.into()));
279    }
280
281    /// Add a key-value entry (mutable reference version)
282    pub fn push_entry(&mut self, key: impl Into<String>, value: impl Into<String>) {
283        self.rows.push(DebugTableRow::Entry {
284            key: key.into(),
285            value: value.into(),
286        });
287    }
288
289    /// Set the cell preview for inspect overlays
290    pub fn cell_preview(mut self, preview: CellPreview) -> Self {
291        self.cell_preview = Some(preview);
292        self
293    }
294
295    /// Set the cell preview (mutable reference version)
296    pub fn set_cell_preview(&mut self, preview: CellPreview) {
297        self.cell_preview = Some(preview);
298    }
299
300    /// Build the final table overlay with the given title
301    pub fn finish(self, title: impl Into<String>) -> DebugTableOverlay {
302        DebugTableOverlay {
303            title: title.into(),
304            rows: self.rows,
305            cell_preview: self.cell_preview,
306        }
307    }
308
309    /// Build as an inspect overlay
310    pub fn finish_inspect(self, title: impl Into<String>) -> DebugOverlay {
311        DebugOverlay::Inspect(self.finish(title))
312    }
313
314    /// Build as a state overlay
315    pub fn finish_state(self, title: impl Into<String>) -> DebugOverlay {
316        DebugOverlay::State(self.finish(title))
317    }
318}
319
320#[cfg(test)]
321mod tests {
322    use super::*;
323
324    #[test]
325    fn test_builder_basic() {
326        let table = DebugTableBuilder::new()
327            .section("Test")
328            .entry("key1", "value1")
329            .entry("key2", "value2")
330            .finish("Test Table");
331
332        assert_eq!(table.title, "Test Table");
333        assert_eq!(table.rows.len(), 3);
334        assert!(table.cell_preview.is_none());
335    }
336
337    #[test]
338    fn test_builder_multiple_sections() {
339        let table = DebugTableBuilder::new()
340            .section("Section 1")
341            .entry("a", "1")
342            .section("Section 2")
343            .entry("b", "2")
344            .finish("Multi-Section");
345
346        assert_eq!(table.rows.len(), 4);
347
348        match &table.rows[0] {
349            DebugTableRow::Section(s) => assert_eq!(s, "Section 1"),
350            _ => panic!("Expected section"),
351        }
352        match &table.rows[2] {
353            DebugTableRow::Section(s) => assert_eq!(s, "Section 2"),
354            _ => panic!("Expected section"),
355        }
356    }
357
358    #[test]
359    fn test_overlay_kinds() {
360        let table = DebugTableBuilder::new().finish("Test");
361
362        let inspect = DebugOverlay::Inspect(table.clone());
363        assert_eq!(inspect.kind(), "inspect");
364        assert!(inspect.table().is_some());
365        assert!(inspect.action_log().is_none());
366
367        let state = DebugOverlay::State(table);
368        assert_eq!(state.kind(), "state");
369
370        let action_log = ActionLogOverlay {
371            title: "Test".to_string(),
372            entries: vec![],
373            selected: 0,
374            scroll_offset: 0,
375        };
376        let log_overlay = DebugOverlay::ActionLog(action_log);
377        assert_eq!(log_overlay.kind(), "action_log");
378        assert!(log_overlay.table().is_none());
379        assert!(log_overlay.action_log().is_some());
380    }
381
382    #[test]
383    fn test_action_log_overlay_scrolling() {
384        let mut overlay = ActionLogOverlay {
385            title: "Test".to_string(),
386            entries: vec![
387                ActionLogDisplayEntry {
388                    sequence: 0,
389                    name: "A".to_string(),
390                    params: "".to_string(),
391                    elapsed: "0ms".to_string(),
392                },
393                ActionLogDisplayEntry {
394                    sequence: 1,
395                    name: "B".to_string(),
396                    params: "x: 1".to_string(),
397                    elapsed: "1ms".to_string(),
398                },
399                ActionLogDisplayEntry {
400                    sequence: 2,
401                    name: "C".to_string(),
402                    params: "y: 2".to_string(),
403                    elapsed: "2ms".to_string(),
404                },
405            ],
406            selected: 0,
407            scroll_offset: 0,
408        };
409
410        assert_eq!(overlay.selected, 0);
411
412        overlay.scroll_down();
413        assert_eq!(overlay.selected, 1);
414
415        overlay.scroll_down();
416        assert_eq!(overlay.selected, 2);
417
418        overlay.scroll_down(); // Should not go past end
419        assert_eq!(overlay.selected, 2);
420
421        overlay.scroll_up();
422        assert_eq!(overlay.selected, 1);
423
424        overlay.scroll_to_top();
425        assert_eq!(overlay.selected, 0);
426
427        overlay.scroll_to_bottom();
428        assert_eq!(overlay.selected, 2);
429    }
430}