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