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}