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