tui_dispatch_debug/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    /// Pretty action parameters for detail view
130    pub params_detail: String,
131    /// Elapsed time display (e.g., "2.3s")
132    pub elapsed: String,
133}
134
135/// Overlay for displaying the action log
136#[derive(Debug, Clone)]
137pub struct ActionLogOverlay {
138    /// Title for the overlay
139    pub title: String,
140    /// Action entries to display
141    pub entries: Vec<ActionLogDisplayEntry>,
142    /// Currently selected entry index (for scrolling)
143    pub selected: usize,
144    /// Scroll offset for visible window
145    pub scroll_offset: usize,
146}
147
148impl ActionLogOverlay {
149    /// Create from an ActionLog reference
150    pub fn from_log(log: &ActionLog, title: impl Into<String>) -> Self {
151        let entries: Vec<_> = log
152            .entries_rev()
153            .map(|e| ActionLogDisplayEntry {
154                sequence: e.sequence,
155                name: e.name.to_string(),
156                params: e.params.clone(),
157                params_detail: e.params_pretty.clone(),
158                elapsed: e.elapsed.clone(),
159            })
160            .collect();
161
162        Self {
163            title: title.into(),
164            entries,
165            selected: 0,
166            scroll_offset: 0,
167        }
168    }
169
170    /// Scroll up (select previous entry)
171    pub fn scroll_up(&mut self) {
172        if self.selected > 0 {
173            self.selected -= 1;
174        }
175    }
176
177    /// Scroll down (select next entry)
178    pub fn scroll_down(&mut self) {
179        if self.selected + 1 < self.entries.len() {
180            self.selected += 1;
181        }
182    }
183
184    /// Jump to the top
185    pub fn scroll_to_top(&mut self) {
186        self.selected = 0;
187    }
188
189    /// Jump to the bottom
190    pub fn scroll_to_bottom(&mut self) {
191        if !self.entries.is_empty() {
192            self.selected = self.entries.len() - 1;
193        }
194    }
195
196    /// Page up
197    pub fn page_up(&mut self, page_size: usize) {
198        self.selected = self.selected.saturating_sub(page_size);
199    }
200
201    /// Page down
202    pub fn page_down(&mut self, page_size: usize) {
203        self.selected = (self.selected + page_size).min(self.entries.len().saturating_sub(1));
204    }
205
206    /// Calculate the scroll offset to keep the selected row visible.
207    pub fn scroll_offset_for(&self, visible_rows: usize) -> usize {
208        if visible_rows == 0 {
209            return 0;
210        }
211        if self.selected >= visible_rows {
212            self.selected - visible_rows + 1
213        } else {
214            0
215        }
216    }
217
218    /// Get the currently selected entry
219    pub fn get_selected(&self) -> Option<&ActionLogDisplayEntry> {
220        self.entries.get(self.selected)
221    }
222
223    /// Create a detail overlay from the selected entry
224    pub fn selected_detail(&self) -> Option<ActionDetailOverlay> {
225        self.get_selected().map(|entry| ActionDetailOverlay {
226            sequence: entry.sequence,
227            name: entry.name.clone(),
228            params: entry.params_detail.clone(),
229            elapsed: entry.elapsed.clone(),
230        })
231    }
232}
233
234/// Builder for constructing debug tables
235///
236/// # Example
237///
238/// ```
239/// use tui_dispatch_debug::debug::{DebugTableBuilder, DebugTableRow};
240///
241/// let table = DebugTableBuilder::new()
242///     .section("Connection")
243///     .entry("host", "localhost")
244///     .entry("port", "6379")
245///     .section("Status")
246///     .entry("connected", "true")
247///     .finish("Connection Info");
248///
249/// assert_eq!(table.title, "Connection Info");
250/// assert_eq!(table.rows.len(), 5);
251/// ```
252#[derive(Debug, Default)]
253pub struct DebugTableBuilder {
254    rows: Vec<DebugTableRow>,
255    cell_preview: Option<CellPreview>,
256}
257
258impl DebugTableBuilder {
259    /// Create a new empty builder
260    pub fn new() -> Self {
261        Self::default()
262    }
263
264    /// Add a section header
265    pub fn section(mut self, title: impl Into<String>) -> Self {
266        self.rows.push(DebugTableRow::Section(title.into()));
267        self
268    }
269
270    /// Add a key-value entry
271    pub fn entry(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
272        self.rows.push(DebugTableRow::Entry {
273            key: key.into(),
274            value: value.into(),
275        });
276        self
277    }
278
279    /// Add a section header (mutable reference version)
280    pub fn push_section(&mut self, title: impl Into<String>) {
281        self.rows.push(DebugTableRow::Section(title.into()));
282    }
283
284    /// Add a key-value entry (mutable reference version)
285    pub fn push_entry(&mut self, key: impl Into<String>, value: impl Into<String>) {
286        self.rows.push(DebugTableRow::Entry {
287            key: key.into(),
288            value: value.into(),
289        });
290    }
291
292    /// Set the cell preview for inspect overlays
293    pub fn cell_preview(mut self, preview: CellPreview) -> Self {
294        self.cell_preview = Some(preview);
295        self
296    }
297
298    /// Set the cell preview (mutable reference version)
299    pub fn set_cell_preview(&mut self, preview: CellPreview) {
300        self.cell_preview = Some(preview);
301    }
302
303    /// Build the final table overlay with the given title
304    pub fn finish(self, title: impl Into<String>) -> DebugTableOverlay {
305        DebugTableOverlay {
306            title: title.into(),
307            rows: self.rows,
308            cell_preview: self.cell_preview,
309        }
310    }
311
312    /// Build as an inspect overlay
313    pub fn finish_inspect(self, title: impl Into<String>) -> DebugOverlay {
314        DebugOverlay::Inspect(self.finish(title))
315    }
316
317    /// Build as a state overlay
318    pub fn finish_state(self, title: impl Into<String>) -> DebugOverlay {
319        DebugOverlay::State(self.finish(title))
320    }
321}
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    #[test]
328    fn test_builder_basic() {
329        let table = DebugTableBuilder::new()
330            .section("Test")
331            .entry("key1", "value1")
332            .entry("key2", "value2")
333            .finish("Test Table");
334
335        assert_eq!(table.title, "Test Table");
336        assert_eq!(table.rows.len(), 3);
337        assert!(table.cell_preview.is_none());
338    }
339
340    #[test]
341    fn test_builder_multiple_sections() {
342        let table = DebugTableBuilder::new()
343            .section("Section 1")
344            .entry("a", "1")
345            .section("Section 2")
346            .entry("b", "2")
347            .finish("Multi-Section");
348
349        assert_eq!(table.rows.len(), 4);
350
351        match &table.rows[0] {
352            DebugTableRow::Section(s) => assert_eq!(s, "Section 1"),
353            _ => panic!("Expected section"),
354        }
355        match &table.rows[2] {
356            DebugTableRow::Section(s) => assert_eq!(s, "Section 2"),
357            _ => panic!("Expected section"),
358        }
359    }
360
361    #[test]
362    fn test_overlay_kinds() {
363        let table = DebugTableBuilder::new().finish("Test");
364
365        let inspect = DebugOverlay::Inspect(table.clone());
366        assert_eq!(inspect.kind(), "inspect");
367        assert!(inspect.table().is_some());
368        assert!(inspect.action_log().is_none());
369
370        let state = DebugOverlay::State(table);
371        assert_eq!(state.kind(), "state");
372
373        let action_log = ActionLogOverlay {
374            title: "Test".to_string(),
375            entries: vec![],
376            selected: 0,
377            scroll_offset: 0,
378        };
379        let log_overlay = DebugOverlay::ActionLog(action_log);
380        assert_eq!(log_overlay.kind(), "action_log");
381        assert!(log_overlay.table().is_none());
382        assert!(log_overlay.action_log().is_some());
383    }
384
385    #[test]
386    fn test_action_log_overlay_scrolling() {
387        let mut overlay = ActionLogOverlay {
388            title: "Test".to_string(),
389            entries: vec![
390                ActionLogDisplayEntry {
391                    sequence: 0,
392                    name: "A".to_string(),
393                    params: "".to_string(),
394                    params_detail: "".to_string(),
395                    elapsed: "0ms".to_string(),
396                },
397                ActionLogDisplayEntry {
398                    sequence: 1,
399                    name: "B".to_string(),
400                    params: "x: 1".to_string(),
401                    params_detail: "x: 1".to_string(),
402                    elapsed: "1ms".to_string(),
403                },
404                ActionLogDisplayEntry {
405                    sequence: 2,
406                    name: "C".to_string(),
407                    params: "y: 2".to_string(),
408                    params_detail: "y: 2".to_string(),
409                    elapsed: "2ms".to_string(),
410                },
411            ],
412            selected: 0,
413            scroll_offset: 0,
414        };
415
416        assert_eq!(overlay.selected, 0);
417
418        overlay.scroll_down();
419        assert_eq!(overlay.selected, 1);
420
421        overlay.scroll_down();
422        assert_eq!(overlay.selected, 2);
423
424        overlay.scroll_down(); // Should not go past end
425        assert_eq!(overlay.selected, 2);
426
427        overlay.scroll_up();
428        assert_eq!(overlay.selected, 1);
429
430        overlay.scroll_to_top();
431        assert_eq!(overlay.selected, 0);
432
433        overlay.scroll_to_bottom();
434        assert_eq!(overlay.selected, 2);
435    }
436}