Skip to main content

vortex_tui/browse/
app.rs

1// SPDX-License-Identifier: Apache-2.0
2// SPDX-FileCopyrightText: Copyright the Vortex contributors
3
4//! Application state and data structures for the TUI browser.
5
6use std::sync::Arc;
7
8use ratatui::prelude::Size;
9use ratatui::widgets::ListState;
10use vortex::array::ArrayRef;
11use vortex::dtype::DType;
12use vortex::error::VortexExpect;
13use vortex::file::Footer;
14use vortex::file::SegmentSpec;
15use vortex::file::VortexFile;
16use vortex::layout::LayoutRef;
17use vortex::layout::VTable;
18use vortex::layout::layouts::flat::Flat;
19use vortex::layout::layouts::zoned::Zoned;
20use vortex::layout::segments::SegmentId;
21use vortex::layout::segments::SegmentSource;
22use vortex::session::VortexSession;
23
24use super::ui::SegmentGridState;
25
26/// The currently active tab in the TUI browser.
27#[derive(Default, Copy, Clone, Eq, PartialEq)]
28pub enum Tab {
29    /// The layout tree browser tab.
30    ///
31    /// Shows the hierarchical structure of layouts in the Vortex file and allows navigation
32    /// through the layout tree.
33    #[default]
34    Layout,
35
36    /// The segment map tab.
37    ///
38    /// Displays a visual representation of how segments are laid out in the file.
39    Segments,
40
41    /// SQL query interface powered by DataFusion.
42    #[cfg(feature = "native")]
43    Query,
44}
45
46/// A navigable pointer into the layout hierarchy of a Vortex file.
47///
48/// The cursor maintains the current position within the layout tree and provides methods to
49/// navigate up and down the hierarchy. It also provides access to layout metadata and segment
50/// information at the current position.
51pub struct LayoutCursor {
52    path: Vec<usize>,
53    footer: Footer,
54    layout: LayoutRef,
55    segment_map: Arc<[SegmentSpec]>,
56    segment_source: Arc<dyn SegmentSource>,
57}
58
59impl LayoutCursor {
60    /// Create a new cursor pointing at the root layout.
61    pub fn new(footer: Footer, segment_source: Arc<dyn SegmentSource>) -> Self {
62        Self {
63            path: Vec::new(),
64            layout: footer.layout().clone(),
65            segment_map: Arc::clone(footer.segment_map()),
66            footer,
67            segment_source,
68        }
69    }
70
71    /// Create a new cursor at a specific path within the layout tree.
72    ///
73    /// The path is a sequence of child indices to traverse from the root.
74    pub fn new_with_path(
75        footer: Footer,
76        segment_source: Arc<dyn SegmentSource>,
77        path: Vec<usize>,
78    ) -> Self {
79        let mut layout = footer.layout().clone();
80
81        // Traverse the layout tree at each element of the path.
82        for component in path.iter().copied() {
83            layout = layout
84                .child(component)
85                .vortex_expect("Failed to get child layout");
86        }
87
88        Self {
89            segment_map: Arc::clone(footer.segment_map()),
90            path,
91            footer,
92            layout,
93            segment_source,
94        }
95    }
96
97    /// Create a new cursor pointing at the n-th child of the current layout.
98    pub fn child(&self, n: usize) -> Self {
99        let mut path = self.path.clone();
100        path.push(n);
101
102        Self::new_with_path(self.footer.clone(), self.segment_source.clone(), path)
103    }
104
105    /// Create a new cursor pointing at the parent of the current layout.
106    ///
107    /// If already at the root, returns a cursor pointing at the root.
108    pub fn parent(&self) -> Self {
109        let mut path = self.path.clone();
110        path.pop();
111
112        Self::new_with_path(self.footer.clone(), self.segment_source.clone(), path)
113    }
114
115    /// Get a human-readable description of the flat layout metadata.
116    ///
117    /// # Panics
118    ///
119    /// Panics if the current layout is not a [`Flat`] layout.
120    pub fn flat_layout_metadata_info(&self) -> String {
121        let flat_layout = self.layout.as_::<Flat>();
122        let metadata = Flat::metadata(flat_layout);
123
124        match metadata.0.array_encoding_tree.as_ref() {
125            Some(tree) => {
126                let size = tree.len();
127                format!(
128                    "Flat Metadata: array_encoding_tree present ({} bytes)",
129                    size
130                )
131            }
132            None => "Flat Metadata: array_encoding_tree not present".to_string(),
133        }
134    }
135
136    /// Get the total size in bytes of all segments reachable from this layout.
137    pub fn total_size(&self) -> usize {
138        self.layout_segments()
139            .iter()
140            .map(|id| self.segment_spec(*id).length as usize)
141            .sum()
142    }
143
144    fn layout_segments(&self) -> Vec<SegmentId> {
145        self.layout
146            .depth_first_traversal()
147            .map(|layout| layout.vortex_expect("Failed to load layout"))
148            .flat_map(|layout| layout.segment_ids().into_iter())
149            .collect()
150    }
151
152    /// Returns `true` if the cursor is currently pointing at a statistics table.
153    ///
154    /// A statistics table is the second child of a [`Zoned`] layout.
155    pub fn is_stats_table(&self) -> bool {
156        let parent = self.parent();
157        parent.layout().is::<Zoned>() && self.path.last().copied().unwrap_or_default() == 1
158    }
159
160    /// Get the data type of the current layout.
161    pub fn dtype(&self) -> &DType {
162        self.layout.dtype()
163    }
164
165    /// Get a reference to the current layout.
166    pub fn layout(&self) -> &LayoutRef {
167        &self.layout
168    }
169
170    /// Get the segment specification for a given segment ID.
171    pub fn segment_spec(&self, id: SegmentId) -> &SegmentSpec {
172        &self.segment_map[*id as usize]
173    }
174}
175
176/// The current input mode of the TUI.
177///
178/// Different modes change how keyboard input is interpreted.
179#[derive(Default, PartialEq, Eq)]
180pub enum KeyMode {
181    /// Normal navigation mode.
182    ///
183    /// The default mode when the TUI starts. Allows browsing through the layout hierarchy using
184    /// arrow keys, vim-style navigation (`h`/`j`/`k`/`l`), and various shortcuts.
185    #[default]
186    Normal,
187
188    /// Search/filter mode.
189    ///
190    /// Activated by pressing `/` or `Ctrl-S`. In this mode, key presses are used to build a fuzzy
191    /// search filter that narrows down the displayed layout children. Press `Esc` or `Ctrl-G` to
192    /// exit search mode.
193    Search,
194}
195
196/// The complete application state for the TUI browser.
197///
198/// This struct holds all state needed to render and interact with the TUI, including:
199/// - The Vortex session and file being browsed
200/// - Navigation state (current cursor position, selected tab)
201/// - Input mode and search filter state
202/// - UI state for lists and grids
203///
204/// The state is preserved when switching between tabs, allowing users to return to their previous
205/// position.
206pub struct AppState {
207    /// The Vortex session used to read array data during rendering.
208    pub session: VortexSession,
209
210    /// The current input mode (normal navigation or search).
211    pub key_mode: KeyMode,
212
213    /// The current search filter string (only used in search mode).
214    pub search_filter: String,
215
216    /// A boolean mask indicating which children match the current search filter.
217    ///
218    /// `None` when no filter is active, `Some(vec)` when filtering where `vec[i]` indicates
219    /// whether child `i` should be shown.
220    pub filter: Option<Vec<bool>>,
221
222    /// The open Vortex file being browsed.
223    pub vxf: VortexFile,
224
225    /// The current position in the layout hierarchy.
226    pub cursor: LayoutCursor,
227
228    /// The currently selected tab.
229    pub current_tab: Tab,
230
231    /// Selection state for the layout children list.
232    pub layouts_list_state: ListState,
233
234    /// State for the segment grid display.
235    pub segment_grid_state: SegmentGridState,
236
237    /// The size of the last rendered frame.
238    pub frame_size: Size,
239
240    /// Vertical scroll offset for the encoding tree display in flat layout view.
241    pub tree_scroll_offset: u16,
242
243    /// Cached array data for the current FlatLayout, loaded asynchronously on WASM.
244    pub cached_flat_array: Option<ArrayRef>,
245
246    /// Cached flatbuffer size for the current FlatLayout, loaded asynchronously on WASM.
247    pub cached_flatbuffer_size: Option<usize>,
248
249    /// State for the Query tab.
250    #[cfg(feature = "native")]
251    pub query_state: super::ui::QueryState,
252
253    /// File path for use in query execution.
254    #[cfg(feature = "native")]
255    pub file_path: String,
256}
257
258impl AppState {
259    /// Create a new application state by opening a Vortex file from a path.
260    ///
261    /// # Errors
262    ///
263    /// Returns an error if the file cannot be opened or read.
264    #[cfg(feature = "native")]
265    pub async fn new(
266        session: &VortexSession,
267        path: impl AsRef<std::path::Path>,
268    ) -> vortex::error::VortexResult<AppState> {
269        use vortex::file::OpenOptionsSessionExt;
270
271        let session = session.clone();
272        let vxf = session.open_options().open_path(path.as_ref()).await?;
273
274        let cursor = LayoutCursor::new(vxf.footer().clone(), vxf.segment_source());
275
276        let file_path = path
277            .as_ref()
278            .to_str()
279            .map(|s| s.to_string())
280            .unwrap_or_default();
281
282        Ok(AppState {
283            session,
284            vxf,
285            cursor,
286            key_mode: KeyMode::default(),
287            search_filter: String::new(),
288            filter: None,
289            current_tab: Tab::default(),
290            layouts_list_state: ListState::default().with_selected(Some(0)),
291            segment_grid_state: SegmentGridState::default(),
292            frame_size: Size::new(0, 0),
293            tree_scroll_offset: 0,
294            cached_flat_array: None,
295            cached_flatbuffer_size: None,
296            query_state: super::ui::QueryState::default(),
297            file_path,
298        })
299    }
300
301    /// Create a new application state from an in-memory buffer.
302    ///
303    /// # Errors
304    ///
305    /// Returns an error if the buffer does not contain a valid Vortex file.
306    #[cfg(target_arch = "wasm32")]
307    pub fn from_buffer(
308        session: VortexSession,
309        buffer: vortex::buffer::ByteBuffer,
310    ) -> vortex::error::VortexResult<AppState> {
311        use vortex::file::OpenOptionsSessionExt;
312
313        let vxf = session.open_options().open_buffer(buffer)?;
314        let cursor = LayoutCursor::new(vxf.footer().clone(), vxf.segment_source());
315
316        Ok(AppState {
317            session,
318            vxf,
319            cursor,
320            key_mode: KeyMode::default(),
321            search_filter: String::new(),
322            filter: None,
323            current_tab: Tab::default(),
324            layouts_list_state: ListState::default().with_selected(Some(0)),
325            segment_grid_state: SegmentGridState::default(),
326            frame_size: Size::new(0, 0),
327            tree_scroll_offset: 0,
328            cached_flat_array: None,
329            cached_flatbuffer_size: None,
330        })
331    }
332
333    /// Clear the current search filter and return to showing all children.
334    pub fn clear_search(&mut self) {
335        self.search_filter.clear();
336        self.filter.take();
337    }
338
339    /// Reset the layout view state after navigating to a different layout.
340    ///
341    /// This resets the list selection to the first item and clears any scroll offset.
342    /// The caller is responsible for awaiting `load_flat_data()` afterward if the
343    /// new layout is a [`Flat`].
344    pub fn reset_layout_view_state(&mut self) {
345        self.layouts_list_state = ListState::default().with_selected(Some(0));
346        self.tree_scroll_offset = 0;
347        self.cached_flat_array = None;
348        self.cached_flatbuffer_size = None;
349    }
350
351    /// Asynchronously load and cache the flat layout array data and flatbuffer size.
352    #[cfg(feature = "native")]
353    pub(crate) async fn load_flat_data(&mut self) {
354        use vortex::array::MaskFuture;
355        use vortex::array::serde::ArrayParts;
356        use vortex::expr::root;
357
358        let layout = &self.cursor.layout().clone();
359        let row_count = layout.row_count();
360
361        // Load the array.
362        let reader = layout
363            .new_reader("".into(), self.vxf.segment_source(), &self.session)
364            .vortex_expect("Failed to create reader");
365        let array = reader
366            .projection_evaluation(
367                &(0..row_count),
368                &root(),
369                MaskFuture::new_true(
370                    usize::try_from(row_count).vortex_expect("row_count overflowed usize"),
371                ),
372            )
373            .vortex_expect("Failed to construct projection")
374            .await
375            .vortex_expect("Failed to read flat array");
376        self.cached_flat_array = Some(array);
377
378        // Load the flatbuffer size.
379        let segment_id = layout.as_::<Flat>().segment_id();
380        let segment = self
381            .cursor
382            .segment_source
383            .request(segment_id)
384            .await
385            .vortex_expect("Failed to read segment");
386        self.cached_flatbuffer_size = Some(
387            ArrayParts::try_from(segment)
388                .vortex_expect("Failed to parse segment")
389                .metadata()
390                .len(),
391        );
392    }
393}