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