Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
824 changes: 398 additions & 426 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions vortex-tui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ version = { workspace = true }

[dependencies]
anyhow = { workspace = true }
arrow-array = { workspace = true }
arrow-schema = { workspace = true }
clap = { workspace = true, features = ["derive"] }
crossterm = { workspace = true }
datafusion = { workspace = true }
env_logger = { version = "0.11" }
flatbuffers = { workspace = true }
futures = { workspace = true, features = ["executor"] }
Expand All @@ -26,9 +29,12 @@ indicatif = { workspace = true, features = ["futures"] }
itertools = { workspace = true }
parquet = { workspace = true, features = ["arrow", "async"] }
ratatui = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
taffy = { workspace = true }
tokio = { workspace = true, features = ["rt-multi-thread"] }
vortex = { workspace = true, features = ["tokio"] }
vortex-datafusion = { workspace = true }

[lints]
workspace = true
Expand Down
18 changes: 18 additions & 0 deletions vortex-tui/src/browse/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ use vortex::layout::segments::SegmentId;
use vortex::layout::segments::SegmentSource;
use vortex::session::VortexSession;

use super::ui::QueryState;
use super::ui::SegmentGridState;

/// The currently active tab in the TUI browser.
Expand All @@ -41,6 +42,9 @@ pub enum Tab {
///
/// Displays a visual representation of how segments are laid out in the file.
Segments,

/// SQL query interface powered by DataFusion.
Query,
}

/// A navigable pointer into the layout hierarchy of a Vortex file.
Expand Down Expand Up @@ -254,6 +258,12 @@ pub struct AppState<'a> {

/// Vertical scroll offset for the encoding tree display in flat layout view.
pub tree_scroll_offset: u16,

/// State for the Query tab
pub query_state: QueryState,

/// File path for use in query execution
pub file_path: String,
}

impl<'a> AppState<'a> {
Expand All @@ -270,6 +280,12 @@ impl<'a> AppState<'a> {

let cursor = LayoutCursor::new(vxf.footer().clone(), vxf.segment_source());

let file_path = path
.as_ref()
.to_str()
.map(|s| s.to_string())
.unwrap_or_default();

Ok(AppState {
session,
vxf,
Expand All @@ -282,6 +298,8 @@ impl<'a> AppState<'a> {
segment_grid_state: SegmentGridState::default(),
frame_size: Size::new(0, 0),
tree_scroll_offset: 0,
query_state: QueryState::default(),
file_path,
})
}

Expand Down
123 changes: 121 additions & 2 deletions vortex-tui/src/browse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ use crossterm::event::KeyCode;
use crossterm::event::KeyEventKind;
use crossterm::event::KeyModifiers;
use ratatui::DefaultTerminal;
use ui::QueryFocus;
use ui::SortDirection;
use ui::render_app;
use vortex::error::VortexExpect;
use vortex::error::VortexResult;
Expand Down Expand Up @@ -81,54 +83,144 @@ fn navigate_layout_down(app: &mut AppState, amount: usize) {
}
}

#[allow(clippy::cognitive_complexity)]
fn handle_normal_mode(app: &mut AppState, event: Event) -> HandleResult {
if let Event::Key(key) = event
&& key.kind == KeyEventKind::Press
{
// Check if we're in Query tab with SQL input focus - handle text input first
let in_sql_input =
app.current_tab == Tab::Query && app.query_state.focus == QueryFocus::SqlInput;

// Handle SQL input mode - most keys should type into the input
if in_sql_input {
match (key.code, key.modifiers) {
// These keys exit/switch even in SQL input mode
(KeyCode::Tab, _) => {
app.current_tab = Tab::Layout;
}
(KeyCode::Esc, _) => {
app.query_state.toggle_focus();
}
(KeyCode::Enter, _) => {
// Execute the SQL query with COUNT(*) for pagination
app.query_state.sort_column = None;
app.query_state.sort_direction = SortDirection::None;
let file_path = app.file_path.clone();
app.query_state
.execute_initial_query(app.session, &file_path);
// Switch focus to results table after executing
app.query_state.focus = QueryFocus::ResultsTable;
}
// Navigation keys
(KeyCode::Left, _) => app.query_state.move_cursor_left(),
(KeyCode::Right, _) => app.query_state.move_cursor_right(),
(KeyCode::Home, _) => app.query_state.move_cursor_start(),
(KeyCode::End, _) => app.query_state.move_cursor_end(),
// Control key shortcuts
(KeyCode::Char('a'), KeyModifiers::CONTROL) => app.query_state.move_cursor_start(),
(KeyCode::Char('e'), KeyModifiers::CONTROL) => app.query_state.move_cursor_end(),
(KeyCode::Char('u'), KeyModifiers::CONTROL) => app.query_state.clear_input(),
(KeyCode::Char('b'), KeyModifiers::CONTROL) => app.query_state.move_cursor_left(),
(KeyCode::Char('f'), KeyModifiers::CONTROL) => app.query_state.move_cursor_right(),
(KeyCode::Char('d'), KeyModifiers::CONTROL) => {
app.query_state.delete_char_forward()
}
// Delete keys
(KeyCode::Backspace, _) => app.query_state.delete_char(),
(KeyCode::Delete, _) => app.query_state.delete_char_forward(),
// All other characters get typed into the input
(KeyCode::Char(c), KeyModifiers::NONE | KeyModifiers::SHIFT) => {
app.query_state.insert_char(c);
}
_ => {}
}
return HandleResult::Continue;
}

// Normal mode handling for all other cases
match (key.code, key.modifiers) {
(KeyCode::Char('q'), _) => {
return HandleResult::Exit;
}
(KeyCode::Tab, _) => {
app.current_tab = match app.current_tab {
Tab::Layout => Tab::Segments,
Tab::Segments => Tab::Layout,
Tab::Segments => Tab::Query,
Tab::Query => Tab::Layout,
};
}

// Query tab: '[' for previous page
(KeyCode::Char('['), KeyModifiers::NONE) => {
if app.current_tab == Tab::Query {
app.query_state
.prev_page(app.session, &app.file_path.clone());
}
}

// Query tab: ']' for next page
(KeyCode::Char(']'), KeyModifiers::NONE) => {
if app.current_tab == Tab::Query {
app.query_state
.next_page(app.session, &app.file_path.clone());
}
}

(KeyCode::Up | KeyCode::Char('k'), _) | (KeyCode::Char('p'), KeyModifiers::CONTROL) => {
match app.current_tab {
Tab::Layout => navigate_layout_up(app, SCROLL_LINE),
Tab::Segments => app.segment_grid_state.scroll_up(SEGMENT_SCROLL_LINE),
Tab::Query => {
app.query_state.table_state.select_previous();
}
}
}
(KeyCode::Down | KeyCode::Char('j'), _)
| (KeyCode::Char('n'), KeyModifiers::CONTROL) => match app.current_tab {
Tab::Layout => navigate_layout_down(app, SCROLL_LINE),
Tab::Segments => app.segment_grid_state.scroll_down(SEGMENT_SCROLL_LINE),
Tab::Query => {
app.query_state.table_state.select_next();
}
},
(KeyCode::PageUp, _) | (KeyCode::Char('v'), KeyModifiers::ALT) => {
match app.current_tab {
Tab::Layout => navigate_layout_up(app, SCROLL_PAGE),
Tab::Segments => app.segment_grid_state.scroll_up(SEGMENT_SCROLL_PAGE),
Tab::Query => {
app.query_state
.prev_page(app.session, &app.file_path.clone());
}
}
}
(KeyCode::PageDown, _) | (KeyCode::Char('v'), KeyModifiers::CONTROL) => {
match app.current_tab {
Tab::Layout => navigate_layout_down(app, SCROLL_PAGE),
Tab::Segments => app.segment_grid_state.scroll_down(SEGMENT_SCROLL_PAGE),
Tab::Query => {
app.query_state
.next_page(app.session, &app.file_path.clone());
}
}
}
(KeyCode::Home, _) | (KeyCode::Char('<'), KeyModifiers::ALT) => match app.current_tab {
Tab::Layout => app.layouts_list_state.select_first(),
Tab::Segments => app
.segment_grid_state
.scroll_left(SEGMENT_SCROLL_HORIZONTAL_JUMP),
Tab::Query => {
app.query_state.table_state.select_first();
}
},
(KeyCode::End, _) | (KeyCode::Char('>'), KeyModifiers::ALT) => match app.current_tab {
Tab::Layout => app.layouts_list_state.select_last(),
Tab::Segments => app
.segment_grid_state
.scroll_right(SEGMENT_SCROLL_HORIZONTAL_JUMP),
Tab::Query => {
app.query_state.table_state.select_last();
}
},
(KeyCode::Enter, _) => {
if app.current_tab == Tab::Layout && app.cursor.layout().nchildren() > 0 {
Expand All @@ -147,18 +239,45 @@ fn handle_normal_mode(app: &mut AppState, event: Event) -> HandleResult {
Tab::Segments => app
.segment_grid_state
.scroll_left(SEGMENT_SCROLL_HORIZONTAL_STEP),
Tab::Query => {
app.query_state.horizontal_scroll =
app.query_state.horizontal_scroll.saturating_sub(1);
}
},
(KeyCode::Right | KeyCode::Char('l'), _) | (KeyCode::Char('b'), KeyModifiers::ALT) => {
match app.current_tab {
Tab::Layout => {}
Tab::Segments => app
.segment_grid_state
.scroll_right(SEGMENT_SCROLL_HORIZONTAL_STEP),
Tab::Query => {
let max_col = app.query_state.column_count().saturating_sub(1);
if app.query_state.horizontal_scroll < max_col {
app.query_state.horizontal_scroll += 1;
}
}
}
}

(KeyCode::Char('/'), _) | (KeyCode::Char('s'), KeyModifiers::CONTROL) => {
app.key_mode = KeyMode::Search;
if app.current_tab != Tab::Query {
app.key_mode = KeyMode::Search;
}
}

(KeyCode::Char('s'), KeyModifiers::NONE) => {
if app.current_tab == Tab::Query {
// Sort by selected column - modifies the SQL query
let col = app.query_state.selected_column();
app.query_state.apply_sort(app.session, col, &app.file_path);
}
}

(KeyCode::Esc, _) => {
if app.current_tab == Tab::Query {
// Toggle focus in Query tab
app.query_state.toggle_focus();
}
}

_ => {}
Expand Down
20 changes: 11 additions & 9 deletions vortex-tui/src/browse/ui/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@
//! UI rendering components for the TUI browser.

mod layouts;
mod query;
mod segments;

use layouts::render_layouts;
pub use query::QueryFocus;
pub use query::QueryState;
pub use query::SortDirection;
use query::render_query;
use ratatui::prelude::*;
use ratatui::widgets::Block;
use ratatui::widgets::BorderType;
Expand Down Expand Up @@ -65,17 +70,13 @@ pub fn render_app(app: &mut AppState<'_>, frame: &mut Frame<'_>) {
let selected_tab = match app.current_tab {
Tab::Layout => 0,
Tab::Segments => 1,
Tab::Query => 2,
};

let tabs = Tabs::new([
"File Layout",
"Segments",
// TODO(aduffy): add SQL query interface
// "Query",
])
.style(Style::default().bold().white())
.highlight_style(Style::default().bold().black().on_white())
.select(Some(selected_tab));
let tabs = Tabs::new(["File Layout", "Segments", "Query"])
.style(Style::default().bold().white())
.highlight_style(Style::default().bold().black().on_white())
.select(Some(selected_tab));

frame.render_widget(tabs, tab_view);

Expand All @@ -85,5 +86,6 @@ pub fn render_app(app: &mut AppState<'_>, frame: &mut Frame<'_>) {
render_layouts(app, app_view, frame.buffer_mut());
}
Tab::Segments => segments_ui(app, app_view, frame.buffer_mut()),
Tab::Query => render_query(app, app_view, frame.buffer_mut()),
}
}
Loading