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
6 changes: 6 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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
20 changes: 18 additions & 2 deletions vortex-tui/src/browse/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use vortex::dtype::DType;
use vortex::error::VortexExpect;
use vortex::error::VortexResult;
use vortex::error::VortexUnwrap;
use vortex::error::vortex_err;
use vortex::file::Footer;
use vortex::file::OpenOptionsSessionExt;
use vortex::file::SegmentSpec;
Expand All @@ -24,6 +25,7 @@ use vortex::layout::segments::SegmentId;
use vortex::layout::segments::SegmentSource;

use crate::SESSION;
use crate::browse::ui::QueryState;
use crate::browse::ui::SegmentGridState;

#[derive(Default, Copy, Clone, Eq, PartialEq)]
Expand All @@ -34,8 +36,9 @@ pub enum Tab {

/// Show a segment map of the file
Segments,
// TODO(aduffy): SQL query page powered by DF
// Query,

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

/// A pointer into the `Layout` hierarchy that can be advanced.
Expand Down Expand Up @@ -199,6 +202,12 @@ pub struct AppState<'a> {

/// Scroll offset for the encoding tree display in FlatLayout 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 AppState<'_> {
Expand All @@ -210,6 +219,11 @@ impl AppState<'_> {

/// Create an app backed from a file path.
pub async fn create_file_app<'a>(path: impl AsRef<Path>) -> VortexResult<AppState<'a>> {
let file_path = path
.as_ref()
.to_str()
.ok_or_else(|| vortex_err!("Path is not valid UTF-8"))?
.to_string();
let vxf = SESSION.open_options().open(path.as_ref()).await?;

let cursor = LayoutCursor::new(vxf.footer().clone(), vxf.segment_source());
Expand All @@ -225,5 +239,7 @@ pub async fn create_file_app<'a>(path: impl AsRef<Path>) -> VortexResult<AppStat
segment_grid_state: SegmentGridState::default(),
frame_size: Size::new(0, 0),
tree_scroll_offset: 0,
query_state: QueryState::default(),
file_path,
})
}
138 changes: 129 additions & 9 deletions vortex-tui/src/browse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ use vortex::error::VortexExpect;
use vortex::error::VortexResult;
use vortex::layout::layouts::flat::FlatVTable;

use crate::browse::ui::QueryFocus;
use crate::browse::ui::SortDirection;

mod app;
mod ui;

Expand Down Expand Up @@ -47,10 +50,61 @@ enum HandleResult {
Exit,
}

#[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(&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'), _) => {
// Close the process down.
Expand All @@ -60,9 +114,25 @@ fn handle_normal_mode(app: &mut AppState, event: Event) -> HandleResult {
// toggle between tabs
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: Ctrl+h for previous page
(KeyCode::Char('h'), KeyModifiers::CONTROL) => {
if app.current_tab == Tab::Query {
app.query_state.prev_page(&app.file_path.clone());
}
}

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

(KeyCode::Up | KeyCode::Char('k'), _) | (KeyCode::Char('p'), KeyModifiers::CONTROL) => {
// We send the key-up to the list state if we're looking at
// the Layouts tab.
Expand All @@ -75,6 +145,9 @@ fn handle_normal_mode(app: &mut AppState, event: Event) -> HandleResult {
}
}
Tab::Segments => app.segment_grid_state.scroll_up(10),
Tab::Query => {
app.query_state.table_state.select_previous();
}
}
}
(KeyCode::Down | KeyCode::Char('j'), _)
Expand All @@ -87,6 +160,9 @@ fn handle_normal_mode(app: &mut AppState, event: Event) -> HandleResult {
}
}
Tab::Segments => app.segment_grid_state.scroll_down(10),
Tab::Query => {
app.query_state.table_state.select_next();
}
},
(KeyCode::PageUp, _) | (KeyCode::Char('v'), KeyModifiers::ALT) => {
match app.current_tab {
Expand All @@ -98,6 +174,9 @@ fn handle_normal_mode(app: &mut AppState, event: Event) -> HandleResult {
}
}
Tab::Segments => app.segment_grid_state.scroll_up(100),
Tab::Query => {
app.query_state.prev_page(&app.file_path.clone());
}
}
}
(KeyCode::PageDown, _) | (KeyCode::Char('v'), KeyModifiers::CONTROL) => {
Expand All @@ -110,25 +189,39 @@ fn handle_normal_mode(app: &mut AppState, event: Event) -> HandleResult {
}
}
Tab::Segments => app.segment_grid_state.scroll_down(100),
Tab::Query => {
app.query_state.next_page(&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(200),
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(200),
Tab::Query => {
app.query_state.table_state.select_last();
}
},
(KeyCode::Enter, _) => {
if app.current_tab == Tab::Layout && app.cursor.layout().nchildren() > 0 {
// Descend into the layout subtree for the selected child.
let selected = app.layouts_list_state.selected().unwrap_or_default();
app.cursor = app.cursor.child(selected);
match app.current_tab {
Tab::Layout => {
if app.cursor.layout().nchildren() > 0 {
// Descend into the layout subtree for the selected child.
let selected = app.layouts_list_state.selected().unwrap_or_default();
app.cursor = app.cursor.child(selected);

// Reset the list scroll state and tree scroll offset.
app.layouts_list_state = ListState::default().with_selected(Some(0));
app.tree_scroll_offset = 0;
// Reset the list scroll state and tree scroll offset.
app.layouts_list_state = ListState::default().with_selected(Some(0));
app.tree_scroll_offset = 0;
}
}
Tab::Query | Tab::Segments => {}
}
}
(KeyCode::Left | KeyCode::Char('h'), _)
Expand All @@ -142,17 +235,44 @@ fn handle_normal_mode(app: &mut AppState, event: Event) -> HandleResult {
app.tree_scroll_offset = 0;
}
Tab::Segments => app.segment_grid_state.scroll_left(20),
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(20),
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(col, &app.file_path);
}
}

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

// Most events not handled
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 @@ -2,9 +2,14 @@
// SPDX-FileCopyrightText: Copyright the Vortex contributors

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 @@ -57,17 +62,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 @@ -77,5 +78,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