1use crossterm::{
2 event::{self, Event, KeyCode, MouseEventKind},
3 execute,
4 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
5};
6use ratatui::{
7 backend::{Backend, CrosstermBackend},
8 layout::{Alignment, Constraint, Direction, Layout},
9 style::{Color, Modifier, Style},
10 widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap},
11 Frame, Terminal,
12};
13use reth_db_api::{
14 table::{Table, TableRow},
15 RawValue,
16};
17use std::{
18 io,
19 time::{Duration, Instant},
20};
21use tracing::error;
22
23static CMDS: [(&str, &str); 6] = [
25 ("q", "Quit"),
26 ("↑", "Entry above"),
27 ("↓", "Entry below"),
28 ("←", "Previous page"),
29 ("→", "Next page"),
30 ("G", "Go to a specific page"),
31];
32
33#[derive(Default, Eq, PartialEq)]
34pub(crate) enum ViewMode {
35 #[default]
37 Normal,
38 GoToPage,
40}
41
42enum Entries<T: Table> {
43 RawValues(Vec<(T::Key, RawValue<T::Value>)>),
45 Values(Vec<TableRow<T>>),
47}
48
49impl<T: Table> Entries<T> {
50 const fn new_with_raw_values(raw_values: bool) -> Self {
53 if raw_values {
54 Self::RawValues(Vec::new())
55 } else {
56 Self::Values(Vec::new())
57 }
58 }
59
60 fn set(&mut self, new_entries: Vec<TableRow<T>>) {
63 match self {
64 Self::RawValues(old_entries) => {
65 *old_entries =
66 new_entries.into_iter().map(|(key, value)| (key, value.into())).collect()
67 }
68 Self::Values(old_entries) => *old_entries = new_entries,
69 }
70 }
71
72 fn len(&self) -> usize {
74 match self {
75 Self::RawValues(entries) => entries.len(),
76 Self::Values(entries) => entries.len(),
77 }
78 }
79
80 const fn iter_keys(&self) -> EntriesKeyIter<'_, T> {
83 EntriesKeyIter { entries: self, index: 0 }
84 }
85}
86
87struct EntriesKeyIter<'a, T: Table> {
88 entries: &'a Entries<T>,
89 index: usize,
90}
91
92impl<'a, T: Table> Iterator for EntriesKeyIter<'a, T> {
93 type Item = &'a T::Key;
94
95 fn next(&mut self) -> Option<Self::Item> {
96 let item = match self.entries {
97 Entries::RawValues(values) => values.get(self.index).map(|(key, _)| key),
98 Entries::Values(values) => values.get(self.index).map(|(key, _)| key),
99 };
100 self.index += 1;
101
102 item
103 }
104}
105
106pub(crate) struct DbListTUI<F, T: Table>
107where
108 F: FnMut(usize, usize) -> Vec<TableRow<T>>,
109{
110 fetch: F,
115 skip: usize,
117 count: usize,
119 total_entries: usize,
121 mode: ViewMode,
123 input: String,
125 list_state: ListState,
127 entries: Entries<T>,
129}
130
131impl<F, T: Table> DbListTUI<F, T>
132where
133 F: FnMut(usize, usize) -> Vec<TableRow<T>>,
134{
135 pub(crate) fn new(
137 fetch: F,
138 skip: usize,
139 count: usize,
140 total_entries: usize,
141 raw: bool,
142 ) -> Self {
143 Self {
144 fetch,
145 skip,
146 count,
147 total_entries,
148 mode: ViewMode::Normal,
149 input: String::new(),
150 list_state: ListState::default(),
151 entries: Entries::new_with_raw_values(raw),
152 }
153 }
154
155 fn next(&mut self) {
157 self.list_state.select(Some(
158 self.list_state
159 .selected()
160 .map(|i| if i >= self.entries.len() - 1 { 0 } else { i + 1 })
161 .unwrap_or(0),
162 ));
163 }
164
165 fn previous(&mut self) {
167 self.list_state.select(Some(
168 self.list_state
169 .selected()
170 .map(|i| if i == 0 { self.entries.len() - 1 } else { i - 1 })
171 .unwrap_or(0),
172 ));
173 }
174
175 fn reset(&mut self) {
176 self.list_state.select(Some(0));
177 }
178
179 fn next_page(&mut self) {
181 if self.skip + self.count < self.total_entries {
182 self.skip += self.count;
183 self.fetch_page();
184 }
185 }
186
187 fn previous_page(&mut self) {
189 if self.skip > 0 {
190 self.skip = self.skip.saturating_sub(self.count);
191 self.fetch_page();
192 }
193 }
194
195 fn go_to_page(&mut self, page: usize) {
197 self.skip = (self.count * page).min(self.total_entries - self.count);
198 self.fetch_page();
199 }
200
201 fn fetch_page(&mut self) {
203 self.entries.set((self.fetch)(self.skip, self.count));
204 self.reset();
205 }
206
207 pub(crate) fn run(mut self) -> eyre::Result<()> {
209 enable_raw_mode()?;
211 let mut stdout = io::stdout();
212 execute!(stdout, EnterAlternateScreen)?;
213 let backend = CrosstermBackend::new(stdout);
214 let mut terminal = Terminal::new(backend)?;
215
216 self.fetch_page();
218
219 let tick_rate = Duration::from_millis(250);
221 let res = event_loop(&mut terminal, &mut self, tick_rate);
222
223 disable_raw_mode()?;
225 execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
226 terminal.show_cursor()?;
227
228 if let Err(err) = res {
230 error!("{err}");
231 }
232
233 Ok(())
234 }
235}
236
237fn event_loop<B: Backend, F, T: Table>(
239 terminal: &mut Terminal<B>,
240 app: &mut DbListTUI<F, T>,
241 tick_rate: Duration,
242) -> io::Result<()>
243where
244 F: FnMut(usize, usize) -> Vec<TableRow<T>>,
245 io::Error: From<B::Error>,
246{
247 let mut last_tick = Instant::now();
248 let mut running = true;
249 while running {
250 terminal.draw(|f| ui(f, app))?;
252
253 let timeout =
255 tick_rate.checked_sub(last_tick.elapsed()).unwrap_or_else(|| Duration::from_secs(0));
256
257 if crossterm::event::poll(timeout)? {
259 running = !handle_event(app, event::read()?)?;
260 }
261
262 if last_tick.elapsed() >= tick_rate {
263 last_tick = Instant::now();
264 }
265 }
266
267 Ok(())
268}
269
270fn handle_event<F, T: Table>(app: &mut DbListTUI<F, T>, event: Event) -> io::Result<bool>
272where
273 F: FnMut(usize, usize) -> Vec<TableRow<T>>,
274{
275 if app.mode == ViewMode::GoToPage {
276 if let Event::Key(key) = event {
277 match key.code {
278 KeyCode::Enter => {
279 let input = std::mem::take(&mut app.input);
280 if let Ok(page) = input.parse() {
281 app.go_to_page(page);
282 }
283 app.mode = ViewMode::Normal;
284 }
285 KeyCode::Char(c) => {
286 app.input.push(c);
287 }
288 KeyCode::Backspace => {
289 app.input.pop();
290 }
291 KeyCode::Esc => app.mode = ViewMode::Normal,
292 _ => {}
293 }
294 }
295
296 return Ok(false)
297 }
298
299 match event {
300 Event::Key(key) => {
301 if key.kind == event::KeyEventKind::Press {
302 match key.code {
303 KeyCode::Char('q') | KeyCode::Char('Q') => return Ok(true),
304 KeyCode::Down => app.next(),
305 KeyCode::Up => app.previous(),
306 KeyCode::Right => app.next_page(),
307 KeyCode::Left => app.previous_page(),
308 KeyCode::Char('G') => {
309 app.mode = ViewMode::GoToPage;
310 }
311 _ => {}
312 }
313 }
314 }
315 Event::Mouse(e) => match e.kind {
316 MouseEventKind::ScrollDown => app.next(),
317 MouseEventKind::ScrollUp => app.previous(),
318 MouseEventKind::Down(_) => {
320 let new_idx = (e.row as usize + app.list_state.offset()).saturating_sub(1);
321 if new_idx < app.entries.len() {
322 app.list_state.select(Some(new_idx));
323 }
324 }
325 _ => {}
326 },
327 _ => {}
328 }
329
330 Ok(false)
331}
332
333fn ui<F, T: Table>(f: &mut Frame<'_>, app: &mut DbListTUI<F, T>)
335where
336 F: FnMut(usize, usize) -> Vec<TableRow<T>>,
337{
338 let outer_chunks = Layout::default()
339 .direction(Direction::Vertical)
340 .constraints([Constraint::Percentage(95), Constraint::Percentage(5)].as_ref())
341 .split(f.area());
342
343 {
345 let inner_chunks = Layout::default()
346 .direction(Direction::Horizontal)
347 .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
348 .split(outer_chunks[0]);
349
350 let key_length = format!("{}", (app.skip + app.count).saturating_sub(1)).len();
351
352 let formatted_keys = app
353 .entries
354 .iter_keys()
355 .enumerate()
356 .map(|(i, k)| {
357 ListItem::new(format!("[{:0>width$}]: {k:?}", i + app.skip, width = key_length))
358 })
359 .collect::<Vec<_>>();
360
361 let key_list = List::new(formatted_keys)
362 .block(Block::default().borders(Borders::ALL).title(format!(
363 "Keys (Showing entries {}-{} out of {} entries)",
364 app.skip,
365 (app.skip + app.entries.len()).saturating_sub(1),
366 app.total_entries
367 )))
368 .style(Style::default().fg(Color::White))
369 .highlight_style(Style::default().fg(Color::Cyan).add_modifier(Modifier::ITALIC))
370 .highlight_symbol("➜ ");
371 f.render_stateful_widget(key_list, inner_chunks[0], &mut app.list_state);
372
373 let value_display = Paragraph::new(
374 app.list_state
375 .selected()
376 .and_then(|selected| {
377 let maybe_serialized = match &app.entries {
378 Entries::RawValues(entries) => {
379 entries.get(selected).map(|(_, v)| serde_json::to_string(v.raw_value()))
380 }
381 Entries::Values(entries) => {
382 entries.get(selected).map(|(_, v)| serde_json::to_string_pretty(v))
383 }
384 };
385 maybe_serialized.map(|ser| {
386 ser.unwrap_or_else(|error| format!("Error serializing value: {error}"))
387 })
388 })
389 .unwrap_or_else(|| "No value selected".to_string()),
390 )
391 .block(Block::default().borders(Borders::ALL).title("Value (JSON)"))
392 .wrap(Wrap { trim: false })
393 .alignment(Alignment::Left);
394 f.render_widget(value_display, inner_chunks[1]);
395 }
396
397 let footer = match app.mode {
399 ViewMode::Normal => Paragraph::new(
400 CMDS.iter().map(|(k, v)| format!("[{k}] {v}")).collect::<Vec<_>>().join(" | "),
401 ),
402 ViewMode::GoToPage => Paragraph::new(format!(
403 "Go to page (max {}): {}",
404 app.total_entries / app.count,
405 app.input
406 )),
407 }
408 .block(Block::default().borders(Borders::ALL))
409 .alignment(match app.mode {
410 ViewMode::Normal => Alignment::Center,
411 ViewMode::GoToPage => Alignment::Left,
412 })
413 .style(Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD));
414 f.render_widget(footer, outer_chunks[1]);
415}