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}
64
65impl DebugOverlay {
66    /// Get the underlying table from the overlay (for Table/State/Inspect)
67    pub fn table(&self) -> Option<&DebugTableOverlay> {
68        match self {
69            DebugOverlay::Inspect(table) | DebugOverlay::State(table) => Some(table),
70            DebugOverlay::ActionLog(_) => None,
71        }
72    }
73
74    /// Get the action log overlay
75    pub fn action_log(&self) -> Option<&ActionLogOverlay> {
76        match self {
77            DebugOverlay::ActionLog(log) => Some(log),
78            _ => None,
79        }
80    }
81
82    /// Get the action log overlay mutably
83    pub fn action_log_mut(&mut self) -> Option<&mut ActionLogOverlay> {
84        match self {
85            DebugOverlay::ActionLog(log) => Some(log),
86            _ => None,
87        }
88    }
89
90    /// Get the overlay kind as a string
91    pub fn kind(&self) -> &'static str {
92        match self {
93            DebugOverlay::Inspect(_) => "inspect",
94            DebugOverlay::State(_) => "state",
95            DebugOverlay::ActionLog(_) => "action_log",
96        }
97    }
98}
99
100// ============================================================================
101// Action Log Overlay
102// ============================================================================
103
104/// A display-ready action log entry
105#[derive(Debug, Clone)]
106pub struct ActionLogDisplayEntry {
107    /// Sequence number
108    pub sequence: u64,
109    /// Action name
110    pub name: String,
111    /// Summary text
112    pub summary: String,
113    /// Elapsed time display (e.g., "2.3s")
114    pub elapsed: String,
115    /// Whether state changed (if known)
116    pub state_changed: Option<bool>,
117}
118
119/// Overlay for displaying the action log
120#[derive(Debug, Clone)]
121pub struct ActionLogOverlay {
122    /// Title for the overlay
123    pub title: String,
124    /// Action entries to display
125    pub entries: Vec<ActionLogDisplayEntry>,
126    /// Currently selected entry index (for scrolling)
127    pub selected: usize,
128    /// Scroll offset for visible window
129    pub scroll_offset: usize,
130}
131
132impl ActionLogOverlay {
133    /// Create from an ActionLog reference
134    pub fn from_log(log: &ActionLog, title: impl Into<String>) -> Self {
135        let entries: Vec<_> = log
136            .entries_rev()
137            .map(|e| ActionLogDisplayEntry {
138                sequence: e.sequence,
139                name: e.name.to_string(),
140                summary: e.summary.clone(),
141                elapsed: e.elapsed_display(),
142                state_changed: e.state_changed,
143            })
144            .collect();
145
146        Self {
147            title: title.into(),
148            entries,
149            selected: 0,
150            scroll_offset: 0,
151        }
152    }
153
154    /// Scroll up (select previous entry)
155    pub fn scroll_up(&mut self) {
156        if self.selected > 0 {
157            self.selected -= 1;
158        }
159    }
160
161    /// Scroll down (select next entry)
162    pub fn scroll_down(&mut self) {
163        if self.selected + 1 < self.entries.len() {
164            self.selected += 1;
165        }
166    }
167
168    /// Jump to the top
169    pub fn scroll_to_top(&mut self) {
170        self.selected = 0;
171    }
172
173    /// Jump to the bottom
174    pub fn scroll_to_bottom(&mut self) {
175        if !self.entries.is_empty() {
176            self.selected = self.entries.len() - 1;
177        }
178    }
179
180    /// Page up
181    pub fn page_up(&mut self, page_size: usize) {
182        self.selected = self.selected.saturating_sub(page_size);
183    }
184
185    /// Page down
186    pub fn page_down(&mut self, page_size: usize) {
187        self.selected = (self.selected + page_size).min(self.entries.len().saturating_sub(1));
188    }
189}
190
191/// Builder for constructing debug tables
192///
193/// # Example
194///
195/// ```
196/// use tui_dispatch_core::debug::{DebugTableBuilder, DebugTableRow};
197///
198/// let table = DebugTableBuilder::new()
199///     .section("Connection")
200///     .entry("host", "localhost")
201///     .entry("port", "6379")
202///     .section("Status")
203///     .entry("connected", "true")
204///     .finish("Connection Info");
205///
206/// assert_eq!(table.title, "Connection Info");
207/// assert_eq!(table.rows.len(), 5);
208/// ```
209#[derive(Debug, Default)]
210pub struct DebugTableBuilder {
211    rows: Vec<DebugTableRow>,
212    cell_preview: Option<CellPreview>,
213}
214
215impl DebugTableBuilder {
216    /// Create a new empty builder
217    pub fn new() -> Self {
218        Self::default()
219    }
220
221    /// Add a section header
222    pub fn section(mut self, title: impl Into<String>) -> Self {
223        self.rows.push(DebugTableRow::Section(title.into()));
224        self
225    }
226
227    /// Add a key-value entry
228    pub fn entry(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
229        self.rows.push(DebugTableRow::Entry {
230            key: key.into(),
231            value: value.into(),
232        });
233        self
234    }
235
236    /// Add a section header (mutable reference version)
237    pub fn push_section(&mut self, title: impl Into<String>) {
238        self.rows.push(DebugTableRow::Section(title.into()));
239    }
240
241    /// Add a key-value entry (mutable reference version)
242    pub fn push_entry(&mut self, key: impl Into<String>, value: impl Into<String>) {
243        self.rows.push(DebugTableRow::Entry {
244            key: key.into(),
245            value: value.into(),
246        });
247    }
248
249    /// Set the cell preview for inspect overlays
250    pub fn cell_preview(mut self, preview: CellPreview) -> Self {
251        self.cell_preview = Some(preview);
252        self
253    }
254
255    /// Set the cell preview (mutable reference version)
256    pub fn set_cell_preview(&mut self, preview: CellPreview) {
257        self.cell_preview = Some(preview);
258    }
259
260    /// Build the final table overlay with the given title
261    pub fn finish(self, title: impl Into<String>) -> DebugTableOverlay {
262        DebugTableOverlay {
263            title: title.into(),
264            rows: self.rows,
265            cell_preview: self.cell_preview,
266        }
267    }
268
269    /// Build as an inspect overlay
270    pub fn finish_inspect(self, title: impl Into<String>) -> DebugOverlay {
271        DebugOverlay::Inspect(self.finish(title))
272    }
273
274    /// Build as a state overlay
275    pub fn finish_state(self, title: impl Into<String>) -> DebugOverlay {
276        DebugOverlay::State(self.finish(title))
277    }
278}
279
280#[cfg(test)]
281mod tests {
282    use super::*;
283
284    #[test]
285    fn test_builder_basic() {
286        let table = DebugTableBuilder::new()
287            .section("Test")
288            .entry("key1", "value1")
289            .entry("key2", "value2")
290            .finish("Test Table");
291
292        assert_eq!(table.title, "Test Table");
293        assert_eq!(table.rows.len(), 3);
294        assert!(table.cell_preview.is_none());
295    }
296
297    #[test]
298    fn test_builder_multiple_sections() {
299        let table = DebugTableBuilder::new()
300            .section("Section 1")
301            .entry("a", "1")
302            .section("Section 2")
303            .entry("b", "2")
304            .finish("Multi-Section");
305
306        assert_eq!(table.rows.len(), 4);
307
308        match &table.rows[0] {
309            DebugTableRow::Section(s) => assert_eq!(s, "Section 1"),
310            _ => panic!("Expected section"),
311        }
312        match &table.rows[2] {
313            DebugTableRow::Section(s) => assert_eq!(s, "Section 2"),
314            _ => panic!("Expected section"),
315        }
316    }
317
318    #[test]
319    fn test_overlay_kinds() {
320        let table = DebugTableBuilder::new().finish("Test");
321
322        let inspect = DebugOverlay::Inspect(table.clone());
323        assert_eq!(inspect.kind(), "inspect");
324        assert!(inspect.table().is_some());
325        assert!(inspect.action_log().is_none());
326
327        let state = DebugOverlay::State(table);
328        assert_eq!(state.kind(), "state");
329
330        let action_log = ActionLogOverlay {
331            title: "Test".to_string(),
332            entries: vec![],
333            selected: 0,
334            scroll_offset: 0,
335        };
336        let log_overlay = DebugOverlay::ActionLog(action_log);
337        assert_eq!(log_overlay.kind(), "action_log");
338        assert!(log_overlay.table().is_none());
339        assert!(log_overlay.action_log().is_some());
340    }
341
342    #[test]
343    fn test_action_log_overlay_scrolling() {
344        let mut overlay = ActionLogOverlay {
345            title: "Test".to_string(),
346            entries: vec![
347                ActionLogDisplayEntry {
348                    sequence: 0,
349                    name: "A".to_string(),
350                    summary: "A".to_string(),
351                    elapsed: "0ms".to_string(),
352                    state_changed: None,
353                },
354                ActionLogDisplayEntry {
355                    sequence: 1,
356                    name: "B".to_string(),
357                    summary: "B".to_string(),
358                    elapsed: "1ms".to_string(),
359                    state_changed: Some(true),
360                },
361                ActionLogDisplayEntry {
362                    sequence: 2,
363                    name: "C".to_string(),
364                    summary: "C".to_string(),
365                    elapsed: "2ms".to_string(),
366                    state_changed: Some(false),
367                },
368            ],
369            selected: 0,
370            scroll_offset: 0,
371        };
372
373        assert_eq!(overlay.selected, 0);
374
375        overlay.scroll_down();
376        assert_eq!(overlay.selected, 1);
377
378        overlay.scroll_down();
379        assert_eq!(overlay.selected, 2);
380
381        overlay.scroll_down(); // Should not go past end
382        assert_eq!(overlay.selected, 2);
383
384        overlay.scroll_up();
385        assert_eq!(overlay.selected, 1);
386
387        overlay.scroll_to_top();
388        assert_eq!(overlay.selected, 0);
389
390        overlay.scroll_to_bottom();
391        assert_eq!(overlay.selected, 2);
392    }
393}