1use ratatui::{
2 buffer::Buffer,
3 layout::{Constraint, Rect},
4 style::{Color, Modifier, Style},
5 widgets::{Block, Row, StatefulWidget, Table, Widget},
6};
7use serde_json::Value;
8
9pub struct VirtualTable<'a> {
11 headers: Vec<&'a str>,
12 data: &'a [Value],
13 widths: Vec<Constraint>,
14 block: Option<Block<'a>>,
15 header_style: Style,
16 row_style: Style,
17 highlight_style: Style,
18 highlight_symbol: &'a str,
19}
20
21impl<'a> VirtualTable<'a> {
22 #[must_use]
23 pub fn new(headers: Vec<&'a str>, data: &'a [Value], widths: Vec<Constraint>) -> Self {
24 Self {
25 headers,
26 data,
27 widths,
28 block: None,
29 header_style: Style::default().fg(Color::Yellow),
30 row_style: Style::default(),
31 highlight_style: Style::default().add_modifier(Modifier::REVERSED),
32 highlight_symbol: ">> ",
33 }
34 }
35
36 #[must_use]
37 pub fn block(mut self, block: Block<'a>) -> Self {
38 self.block = Some(block);
39 self
40 }
41
42 #[must_use]
43 pub fn header_style(mut self, style: Style) -> Self {
44 self.header_style = style;
45 self
46 }
47
48 #[must_use]
49 pub fn highlight_style(mut self, style: Style) -> Self {
50 self.highlight_style = style;
51 self
52 }
53}
54
55#[derive(Default, Clone)]
56pub struct VirtualTableState {
57 pub offset: usize,
59 pub selected: usize,
61 pub visible_rows: usize,
63}
64
65impl VirtualTableState {
66 #[must_use]
67 pub fn new() -> Self {
68 Self::default()
69 }
70
71 pub fn select(&mut self, index: usize) {
72 self.selected = index;
73 }
74
75 pub fn scroll_up(&mut self, amount: usize) {
76 self.selected = self.selected.saturating_sub(amount);
77
78 if self.selected >= self.offset && self.selected < self.offset + self.visible_rows {
80 return;
81 }
82
83 if self.selected < self.offset {
85 self.offset = self.selected;
86 }
87 }
88
89 pub fn scroll_down(&mut self, amount: usize, total_rows: usize) {
90 self.selected = (self.selected + amount).min(total_rows.saturating_sub(1));
91
92 if self.selected >= self.offset && self.selected < self.offset + self.visible_rows {
94 return;
95 }
96
97 if self.selected >= self.offset + self.visible_rows {
99 self.offset = self.selected.saturating_sub(self.visible_rows - 1);
100 }
101 }
102
103 pub fn page_up(&mut self) {
104 let page_size = self.visible_rows.saturating_sub(1);
105 self.scroll_up(page_size);
106 }
107
108 pub fn page_down(&mut self, total_rows: usize) {
109 let page_size = self.visible_rows.saturating_sub(1);
110 self.scroll_down(page_size, total_rows);
111 }
112
113 pub fn goto_top(&mut self) {
114 self.selected = 0;
115 self.offset = 0;
116 }
117
118 pub fn goto_bottom(&mut self, total_rows: usize) {
119 self.selected = total_rows.saturating_sub(1);
120 if total_rows > self.visible_rows {
123 self.offset = total_rows.saturating_sub(self.visible_rows);
124 } else {
125 self.offset = 0;
126 }
127 }
128}
129
130impl StatefulWidget for VirtualTable<'_> {
131 type State = VirtualTableState;
132
133 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
134 let inner_area = if let Some(ref block) = self.block {
136 let inner = block.inner(area);
137 block.render(area, buf);
138 inner
139 } else {
140 area
141 };
142
143 let available_height = inner_area.height.saturating_sub(2); state.visible_rows = available_height as usize;
146
147 let end_row = (state.offset + state.visible_rows).min(self.data.len());
149 let visible_slice = &self.data[state.offset..end_row];
150
151 let header_cells: Vec<ratatui::widgets::Cell> = self
153 .headers
154 .iter()
155 .map(|h| ratatui::widgets::Cell::from(*h).style(self.header_style))
156 .collect();
157 let header = Row::new(header_cells).height(1).bottom_margin(1);
158
159 let rows: Vec<Row> = visible_slice
161 .iter()
162 .enumerate()
163 .map(|(idx, record)| {
164 let absolute_idx = state.offset + idx;
165 let is_selected = absolute_idx == state.selected;
166
167 let cells: Vec<ratatui::widgets::Cell> = self
168 .headers
169 .iter()
170 .map(|field| {
171 if let Some(obj) = record.as_object() {
172 match obj.get(*field) {
173 Some(Value::String(s)) => ratatui::widgets::Cell::from(s.as_str()),
174 Some(Value::Number(n)) => {
175 ratatui::widgets::Cell::from(n.to_string())
176 }
177 Some(Value::Bool(b)) => ratatui::widgets::Cell::from(b.to_string()),
178 Some(Value::Null) => ratatui::widgets::Cell::from("NULL")
179 .style(Style::default().fg(Color::Gray)),
180 Some(v) => ratatui::widgets::Cell::from(v.to_string()),
181 None => ratatui::widgets::Cell::from(""),
182 }
183 } else {
184 ratatui::widgets::Cell::from("")
185 }
186 })
187 .collect();
188
189 let mut row = Row::new(cells).height(1);
190 if is_selected {
191 row = row.style(self.highlight_style);
192 }
193 row
194 })
195 .collect();
196
197 let relative_selected = if state.selected >= state.offset && state.selected < end_row {
199 Some(state.selected - state.offset)
200 } else {
201 None
202 };
203
204 let mut table_state = ratatui::widgets::TableState::default();
206 table_state.select(relative_selected);
207
208 let table = Table::new(rows, self.widths.clone())
210 .header(header)
211 .highlight_symbol(self.highlight_symbol);
212
213 <Table as StatefulWidget>::render(table, inner_area, buf, &mut table_state);
214 }
215}