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