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}