1use app::AppState;
7use app::KeyMode;
8use app::Tab;
9use input::InputEvent;
10use input::InputKeyCode;
11use vortex::error::VortexExpect;
12use vortex::layout::layouts::flat::Flat;
13
14pub mod app;
15pub(crate) mod input;
16pub mod ui;
17
18const SCROLL_LINE: usize = 1;
20const SCROLL_PAGE: usize = 10;
22const SEGMENT_SCROLL_LINE: usize = 10;
24const SEGMENT_SCROLL_PAGE: usize = 100;
26const SEGMENT_SCROLL_HORIZONTAL_STEP: usize = 20;
28const SEGMENT_SCROLL_HORIZONTAL_JUMP: usize = 200;
30
31pub(crate) enum HandleResult {
32 Continue,
33 Exit,
34}
35
36fn navigate_layout_up(app: &mut AppState, amount: usize) {
38 let amount_u16 = amount.try_into().unwrap_or(u16::MAX);
39 if app.cursor.layout().is::<Flat>() {
40 app.tree_scroll_offset = app.tree_scroll_offset.saturating_sub(amount_u16);
41 } else {
42 app.layouts_list_state.scroll_up_by(amount_u16);
43 }
44}
45
46fn navigate_layout_down(app: &mut AppState, amount: usize) {
48 let amount_u16 = amount.try_into().unwrap_or(u16::MAX);
49 if app.cursor.layout().is::<Flat>() {
50 app.tree_scroll_offset = app.tree_scroll_offset.saturating_add(amount_u16);
51 } else {
52 app.layouts_list_state.scroll_down_by(amount_u16);
53 }
54}
55
56#[allow(clippy::cognitive_complexity)]
60pub(crate) fn handle_normal_mode(app: &mut AppState, event: InputEvent) -> HandleResult {
61 #[cfg(feature = "native")]
63 {
64 use ui::QueryFocus;
65 use ui::SortDirection;
66
67 let in_sql_input =
68 app.current_tab == Tab::Query && app.query_state.focus == QueryFocus::SqlInput;
69
70 if in_sql_input {
71 match (&event.code, event.ctrl, event.alt, event.shift) {
72 (InputKeyCode::Tab, ..) => {
73 app.current_tab = Tab::Layout;
74 }
75 (InputKeyCode::Esc, ..) => {
76 app.query_state.toggle_focus();
77 }
78 (InputKeyCode::Enter, ..) => {
79 app.query_state.sort_column = None;
80 app.query_state.sort_direction = SortDirection::None;
81 app.query_state.prepare_initial_query();
82 app.query_state.focus = QueryFocus::ResultsTable;
83 }
84 (InputKeyCode::Left, ..) => app.query_state.move_cursor_left(),
85 (InputKeyCode::Right, ..) => app.query_state.move_cursor_right(),
86 (InputKeyCode::Home, ..) => app.query_state.move_cursor_start(),
87 (InputKeyCode::End, ..) => app.query_state.move_cursor_end(),
88 (InputKeyCode::Char('a'), true, ..) => app.query_state.move_cursor_start(),
89 (InputKeyCode::Char('e'), true, ..) => app.query_state.move_cursor_end(),
90 (InputKeyCode::Char('u'), true, ..) => app.query_state.clear_input(),
91 (InputKeyCode::Char('b'), true, ..) => app.query_state.move_cursor_left(),
92 (InputKeyCode::Char('f'), true, ..) => app.query_state.move_cursor_right(),
93 (InputKeyCode::Char('d'), true, ..) => app.query_state.delete_char_forward(),
94 (InputKeyCode::Backspace, ..) => app.query_state.delete_char(),
95 (InputKeyCode::Delete, ..) => app.query_state.delete_char_forward(),
96 (InputKeyCode::Char(c), false, false, _) => {
97 app.query_state.insert_char(*c);
98 }
99 _ => {}
100 }
101 return HandleResult::Continue;
102 }
103 }
104
105 match (&event.code, event.ctrl, event.alt, event.shift) {
106 (InputKeyCode::Char('q'), ..) => {
107 return HandleResult::Exit;
108 }
109 (InputKeyCode::Tab, ..) => {
110 app.current_tab = match app.current_tab {
111 Tab::Layout => Tab::Segments,
112 #[cfg(feature = "native")]
113 Tab::Segments => Tab::Query,
114 #[cfg(feature = "native")]
115 Tab::Query => Tab::Layout,
116 #[cfg(not(feature = "native"))]
117 Tab::Segments => Tab::Layout,
118 };
119 }
120
121 #[cfg(feature = "native")]
122 (InputKeyCode::Char('['), false, false, _) => {
123 if app.current_tab == Tab::Query {
124 app.query_state.prepare_prev_page();
125 }
126 }
127
128 #[cfg(feature = "native")]
129 (InputKeyCode::Char(']'), false, false, _) => {
130 if app.current_tab == Tab::Query {
131 app.query_state.prepare_next_page();
132 }
133 }
134
135 (InputKeyCode::Up, ..)
136 | (InputKeyCode::Char('k'), false, false, _)
137 | (InputKeyCode::Char('p'), true, ..) => match app.current_tab {
138 Tab::Layout => navigate_layout_up(app, SCROLL_LINE),
139 Tab::Segments => app.segment_grid_state.scroll_up(SEGMENT_SCROLL_LINE),
140 #[cfg(feature = "native")]
141 Tab::Query => {
142 app.query_state.table_state.select_previous();
143 }
144 },
145 (InputKeyCode::Down, ..)
146 | (InputKeyCode::Char('j'), false, false, _)
147 | (InputKeyCode::Char('n'), true, ..) => match app.current_tab {
148 Tab::Layout => navigate_layout_down(app, SCROLL_LINE),
149 Tab::Segments => app.segment_grid_state.scroll_down(SEGMENT_SCROLL_LINE),
150 #[cfg(feature = "native")]
151 Tab::Query => {
152 app.query_state.table_state.select_next();
153 }
154 },
155 (InputKeyCode::PageUp, ..) | (InputKeyCode::Char('v'), _, true, _) => {
156 match app.current_tab {
157 Tab::Layout => navigate_layout_up(app, SCROLL_PAGE),
158 Tab::Segments => app.segment_grid_state.scroll_up(SEGMENT_SCROLL_PAGE),
159 #[cfg(feature = "native")]
160 Tab::Query => {
161 app.query_state.prepare_prev_page();
162 }
163 }
164 }
165 (InputKeyCode::PageDown, ..) | (InputKeyCode::Char('v'), true, ..) => {
166 match app.current_tab {
167 Tab::Layout => navigate_layout_down(app, SCROLL_PAGE),
168 Tab::Segments => app.segment_grid_state.scroll_down(SEGMENT_SCROLL_PAGE),
169 #[cfg(feature = "native")]
170 Tab::Query => {
171 app.query_state.prepare_next_page();
172 }
173 }
174 }
175 (InputKeyCode::Home, ..) | (InputKeyCode::Char('<'), _, true, _) => match app.current_tab {
176 Tab::Layout => app.layouts_list_state.select_first(),
177 Tab::Segments => app
178 .segment_grid_state
179 .scroll_left(SEGMENT_SCROLL_HORIZONTAL_JUMP),
180 #[cfg(feature = "native")]
181 Tab::Query => {
182 app.query_state.table_state.select_first();
183 }
184 },
185 (InputKeyCode::End, ..) | (InputKeyCode::Char('>'), _, true, _) => match app.current_tab {
186 Tab::Layout => app.layouts_list_state.select_last(),
187 Tab::Segments => app
188 .segment_grid_state
189 .scroll_right(SEGMENT_SCROLL_HORIZONTAL_JUMP),
190 #[cfg(feature = "native")]
191 Tab::Query => {
192 app.query_state.table_state.select_last();
193 }
194 },
195 (InputKeyCode::Enter, ..) => {
196 if app.current_tab == Tab::Layout && app.cursor.layout().nchildren() > 0 {
197 let selected = app.layouts_list_state.selected().unwrap_or_default();
198 app.cursor = app.cursor.child(selected);
199 app.reset_layout_view_state();
200 }
201 }
202 (InputKeyCode::Left, ..)
203 | (InputKeyCode::Char('h'), false, false, _)
204 | (InputKeyCode::Char('b'), true, ..) => match app.current_tab {
205 Tab::Layout => {
206 app.cursor = app.cursor.parent();
207 app.reset_layout_view_state();
208 }
209 Tab::Segments => app
210 .segment_grid_state
211 .scroll_left(SEGMENT_SCROLL_HORIZONTAL_STEP),
212 #[cfg(feature = "native")]
213 Tab::Query => {
214 app.query_state.horizontal_scroll =
215 app.query_state.horizontal_scroll.saturating_sub(1);
216 }
217 },
218 (InputKeyCode::Right, ..)
219 | (InputKeyCode::Char('l'), false, false, _)
220 | (InputKeyCode::Char('b'), _, true, _) => match app.current_tab {
221 Tab::Layout => {}
222 Tab::Segments => app
223 .segment_grid_state
224 .scroll_right(SEGMENT_SCROLL_HORIZONTAL_STEP),
225 #[cfg(feature = "native")]
226 Tab::Query => {
227 let max_col = app.query_state.column_count().saturating_sub(1);
228 if app.query_state.horizontal_scroll < max_col {
229 app.query_state.horizontal_scroll += 1;
230 }
231 }
232 },
233
234 (InputKeyCode::Char('/'), ..) | (InputKeyCode::Char('s'), true, ..) => {
235 #[cfg(feature = "native")]
236 if app.current_tab == Tab::Query {
237 } else {
239 app.key_mode = KeyMode::Search;
240 }
241 #[cfg(not(feature = "native"))]
242 {
243 app.key_mode = KeyMode::Search;
244 }
245 }
246
247 #[cfg(feature = "native")]
248 (InputKeyCode::Char('s'), false, false, _) => {
249 if app.current_tab == Tab::Query {
250 let col = app.query_state.selected_column();
251 app.query_state.prepare_sort(col);
252 }
253 }
254
255 #[cfg(feature = "native")]
256 (InputKeyCode::Esc, ..) => {
257 if app.current_tab == Tab::Query {
258 app.query_state.toggle_focus();
259 }
260 }
261
262 _ => {}
263 }
264
265 HandleResult::Continue
266}
267
268pub(crate) fn handle_search_mode(app: &mut AppState, event: InputEvent) -> HandleResult {
270 match (&event.code, event.ctrl, event.alt, event.shift) {
271 (InputKeyCode::Esc, ..) | (InputKeyCode::Char('g'), true, ..) => {
272 app.key_mode = KeyMode::Normal;
273 app.clear_search();
274 }
275
276 (InputKeyCode::Up, ..) | (InputKeyCode::Char('p'), true, ..) => {
277 if app.current_tab == Tab::Layout {
278 navigate_layout_up(app, SCROLL_LINE);
279 }
280 }
281 (InputKeyCode::Down, ..) | (InputKeyCode::Char('n'), true, ..) => {
282 if app.current_tab == Tab::Layout {
283 navigate_layout_down(app, SCROLL_LINE);
284 }
285 }
286 (InputKeyCode::PageUp, ..) | (InputKeyCode::Char('v'), _, true, _) => {
287 if app.current_tab == Tab::Layout {
288 navigate_layout_up(app, SCROLL_PAGE);
289 }
290 }
291 (InputKeyCode::PageDown, ..) | (InputKeyCode::Char('v'), true, ..) => {
292 if app.current_tab == Tab::Layout {
293 navigate_layout_down(app, SCROLL_PAGE);
294 }
295 }
296 (InputKeyCode::Home, ..) | (InputKeyCode::Char('<'), _, true, _) => {
297 if app.current_tab == Tab::Layout {
298 app.layouts_list_state.select_first();
299 }
300 }
301 (InputKeyCode::End, ..) | (InputKeyCode::Char('>'), _, true, _) => {
302 if app.current_tab == Tab::Layout {
303 app.layouts_list_state.select_last();
304 }
305 }
306
307 (InputKeyCode::Enter, ..) => {
308 if app.current_tab == Tab::Layout
309 && app.cursor.layout().nchildren() > 0
310 && let Some(selected) = app.layouts_list_state.selected()
311 {
312 app.cursor = match app.filter.as_ref() {
313 None => app.cursor.child(selected),
314 Some(filter) => {
315 let child_idx = filter
316 .iter()
317 .enumerate()
318 .filter_map(|(idx, show)| show.then_some(idx))
319 .nth(selected)
320 .vortex_expect("There must be a selected item in the filter");
321
322 app.cursor.child(child_idx)
323 }
324 };
325
326 app.reset_layout_view_state();
327 app.clear_search();
328 app.key_mode = KeyMode::Normal;
329 }
330 }
331
332 (InputKeyCode::Backspace, ..) | (InputKeyCode::Char('h'), true, ..) => {
333 app.search_filter.pop();
334 }
335
336 (InputKeyCode::Char(c), false, false, _) => {
337 app.layouts_list_state.select_first();
338 app.search_filter.push(*c);
339 }
340
341 _ => {}
342 }
343
344 HandleResult::Continue
345}
346
347#[cfg(feature = "native")]
350mod native {
351 use crossterm::event::Event;
352 use crossterm::event::EventStream;
353 use crossterm::event::KeyEventKind;
354 use futures::StreamExt;
355 use ratatui::DefaultTerminal;
356 use vortex::error::VortexResult;
357 use vortex::session::VortexSession;
358
359 use super::ui::render_app;
360 use super::*;
361
362 async fn run(mut terminal: DefaultTerminal, mut app: AppState) -> VortexResult<()> {
363 if app.cursor.layout().is::<Flat>() {
365 app.load_flat_data().await;
366 }
367
368 let mut events = EventStream::new();
369 loop {
370 terminal.draw(|frame| render_app(&mut app, frame))?;
371
372 let pending_rx = app.query_state.pending_rx.take();
375
376 let event = if let Some(mut rx) = pending_rx {
377 tokio::select! {
378 event = events.next() => {
379 app.query_state.pending_rx = Some(rx);
381 event
382 }
383 result = &mut rx => {
384 if let Ok(result) = result {
385 app.query_state.apply_query_result(result);
386 }
387 continue;
389 }
390 }
391 } else {
392 events.next().await
393 };
394
395 let Some(raw_event) = event else {
396 break;
397 };
398 let raw_event = raw_event?;
399
400 if let Event::Key(key) = raw_event {
401 if key.kind != KeyEventKind::Press {
402 continue;
403 }
404
405 let input = InputEvent::from(key);
406 let result = match app.key_mode {
407 KeyMode::Normal => handle_normal_mode(&mut app, input),
408 KeyMode::Search => handle_search_mode(&mut app, input),
409 };
410
411 if matches!(result, HandleResult::Exit) {
412 return Ok(());
413 }
414
415 if app.cursor.layout().is::<Flat>() && app.cached_flat_array.is_none() {
417 app.load_flat_data().await;
418 }
419
420 app.query_state.spawn_pending(&app.session, &app.file_path);
422 }
423 }
424 Ok(())
425 }
426
427 pub async fn exec_tui(
433 session: &VortexSession,
434 file: impl AsRef<std::path::Path>,
435 ) -> VortexResult<()> {
436 let app = AppState::new(session, file).await?;
437
438 let mut terminal = ratatui::init();
439 terminal.clear()?;
440
441 run(terminal, app).await?;
442
443 ratatui::restore();
444 Ok(())
445 }
446}
447
448#[cfg(feature = "native")]
449pub use native::exec_tui;