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::SegmentGridState;
29
30/// The currently active tab in the TUI browser.
31#[derive(Default, Copy, Clone, Eq, PartialEq)]
32pub enum Tab {
33 /// The layout tree browser tab.
34 ///
35 /// Shows the hierarchical structure of layouts in the Vortex file and allows navigation
36 /// through the layout tree.
37 #[default]
38 Layout,
39
40 /// The segment map tab.
41 ///
42 /// Displays a visual representation of how segments are laid out in the file.
43 Segments,
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 the size of the array flatbuffer for this layout.
116 ///
117 /// # Panics
118 ///
119 /// Panics if the current layout is not a [`FlatVTable`] layout.
120 pub fn flatbuffer_size(&self) -> usize {
121 let segment_id = self.layout.as_::<FlatVTable>().segment_id();
122 let segment = block_on(self.segment_source.request(segment_id))
123 .vortex_expect("operation should succeed in TUI");
124 ArrayParts::try_from(segment)
125 .vortex_expect("operation should succeed in TUI")
126 .metadata()
127 .len()
128 }
129
130 /// Get a human-readable description of the flat layout metadata.
131 ///
132 /// # Panics
133 ///
134 /// Panics if the current layout is not a [`FlatVTable`] layout.
135 pub fn flat_layout_metadata_info(&self) -> String {
136 let flat_layout = self.layout.as_::<FlatVTable>();
137 let metadata = FlatVTable::metadata(flat_layout);
138
139 match metadata.0.array_encoding_tree.as_ref() {
140 Some(tree) => {
141 let size = tree.len();
142 format!(
143 "Flat Metadata: array_encoding_tree present ({} bytes)",
144 size
145 )
146 }
147 None => "Flat Metadata: array_encoding_tree not present".to_string(),
148 }
149 }
150
151 /// Get the total size in bytes of all segments reachable from this layout.
152 pub fn total_size(&self) -> usize {
153 self.layout_segments()
154 .iter()
155 .map(|id| self.segment_spec(*id).length as usize)
156 .sum()
157 }
158
159 fn layout_segments(&self) -> Vec<SegmentId> {
160 self.layout
161 .depth_first_traversal()
162 .map(|layout| layout.vortex_expect("Failed to load layout"))
163 .flat_map(|layout| layout.segment_ids().into_iter())
164 .collect()
165 }
166
167 /// Returns `true` if the cursor is currently pointing at a statistics table.
168 ///
169 /// A statistics table is the second child of a [`ZonedVTable`] layout.
170 pub fn is_stats_table(&self) -> bool {
171 let parent = self.parent();
172 parent.layout().is::<ZonedVTable>() && self.path.last().copied().unwrap_or_default() == 1
173 }
174
175 /// Get the data type of the current layout.
176 pub fn dtype(&self) -> &DType {
177 self.layout.dtype()
178 }
179
180 /// Get a reference to the current layout.
181 pub fn layout(&self) -> &LayoutRef {
182 &self.layout
183 }
184
185 /// Get the segment specification for a given segment ID.
186 pub fn segment_spec(&self, id: SegmentId) -> &SegmentSpec {
187 &self.segment_map[*id as usize]
188 }
189}
190
191/// The current input mode of the TUI.
192///
193/// Different modes change how keyboard input is interpreted.
194#[derive(Default, PartialEq, Eq)]
195pub enum KeyMode {
196 /// Normal navigation mode.
197 ///
198 /// The default mode when the TUI starts. Allows browsing through the layout hierarchy using
199 /// arrow keys, vim-style navigation (`h`/`j`/`k`/`l`), and various shortcuts.
200 #[default]
201 Normal,
202
203 /// Search/filter mode.
204 ///
205 /// Activated by pressing `/` or `Ctrl-S`. In this mode, key presses are used to build a fuzzy
206 /// search filter that narrows down the displayed layout children. Press `Esc` or `Ctrl-G` to
207 /// exit search mode.
208 Search,
209}
210
211/// The complete application state for the TUI browser.
212///
213/// This struct holds all state needed to render and interact with the TUI, including:
214/// - The Vortex session and file being browsed
215/// - Navigation state (current cursor position, selected tab)
216/// - Input mode and search filter state
217/// - UI state for lists and grids
218///
219/// The state is preserved when switching between tabs, allowing users to return to their previous
220/// position.
221pub struct AppState<'a> {
222 /// The Vortex session used to read array data during rendering.
223 pub session: &'a VortexSession,
224
225 /// The current input mode (normal navigation or search).
226 pub key_mode: KeyMode,
227
228 /// The current search filter string (only used in search mode).
229 pub search_filter: String,
230
231 /// A boolean mask indicating which children match the current search filter.
232 ///
233 /// `None` when no filter is active, `Some(vec)` when filtering where `vec[i]` indicates
234 /// whether child `i` should be shown.
235 pub filter: Option<Vec<bool>>,
236
237 /// The open Vortex file being browsed.
238 pub vxf: VortexFile,
239
240 /// The current position in the layout hierarchy.
241 pub cursor: LayoutCursor,
242
243 /// The currently selected tab.
244 pub current_tab: Tab,
245
246 /// Selection state for the layout children list.
247 pub layouts_list_state: ListState,
248
249 /// State for the segment grid display.
250 pub segment_grid_state: SegmentGridState<'a>,
251
252 /// The size of the last rendered frame.
253 pub frame_size: Size,
254
255 /// Vertical scroll offset for the encoding tree display in flat layout view.
256 pub tree_scroll_offset: u16,
257}
258
259impl<'a> AppState<'a> {
260 /// Create a new application state by opening a Vortex file.
261 ///
262 /// # Errors
263 ///
264 /// Returns an error if the file cannot be opened or read.
265 pub async fn new(
266 session: &'a VortexSession,
267 path: impl AsRef<Path>,
268 ) -> VortexResult<AppState<'a>> {
269 let vxf = session.open_options().open(path.as_ref()).await?;
270
271 let cursor = LayoutCursor::new(vxf.footer().clone(), vxf.segment_source());
272
273 Ok(AppState {
274 session,
275 vxf,
276 cursor,
277 key_mode: KeyMode::default(),
278 search_filter: String::new(),
279 filter: None,
280 current_tab: Tab::default(),
281 layouts_list_state: ListState::default().with_selected(Some(0)),
282 segment_grid_state: SegmentGridState::default(),
283 frame_size: Size::new(0, 0),
284 tree_scroll_offset: 0,
285 })
286 }
287
288 /// Clear the current search filter and return to showing all children.
289 pub fn clear_search(&mut self) {
290 self.search_filter.clear();
291 self.filter.take();
292 }
293
294 /// Reset the layout view state after navigating to a different layout.
295 ///
296 /// This resets the list selection to the first item and clears any scroll offset.
297 pub fn reset_layout_view_state(&mut self) {
298 self.layouts_list_state = ListState::default().with_selected(Some(0));
299 self.tree_scroll_offset = 0;
300 }
301}