Skip to main content

lutra_bin/table/
mod.rs

1//! Table rendering for Lutra binary data.
2//!
3//! Provides ASCII table rendering with support for nested tuples and arrays.
4//! See `tabular.AGENTS.md` for design documentation.
5
6#![cfg(feature = "std")]
7
8mod format;
9mod iterate;
10pub mod layout;
11mod render;
12
13use crate::ir;
14use crate::tabular::TabularReader;
15
16/// Wraps [`TabularReader`] and recursively flattens nested tuples into leaf cells.
17#[derive(Clone)]
18pub struct Table<'d, 't> {
19    reader: TabularReader<'d, 't>,
20}
21
22/// Configuration for table rendering.
23#[derive(Debug, Clone)]
24pub struct Config {
25    /// Maximum column width (default: 20).
26    pub max_col_width: usize,
27    /// Maximum array items to show (default: 3).
28    pub max_array_items: usize,
29    /// Number of rows to sample for layout (None = scan all, default: Some(100)).
30    pub sample_rows: Option<usize>,
31}
32
33impl Default for Config {
34    fn default() -> Self {
35        Self {
36            max_col_width: 20,
37            max_array_items: 3,
38            sample_rows: Some(100),
39        }
40    }
41}
42
43impl<'d, 't> Table<'d, 't> {
44    pub fn new(data: &'d [u8], ty: &'t ir::Ty, ty_defs: &'t [ir::TyDef]) -> Self {
45        Self {
46            reader: TabularReader::new(data, ty, ty_defs),
47        }
48    }
49
50    pub fn render(self) -> String {
51        self.render_with_config(&Config::default())
52    }
53
54    pub fn render_with_config(self, config: &Config) -> String {
55        // Pass 1: compute layout
56        let layout = self.clone().compute_layout(config);
57
58        // Pass 2: render
59        render::render(self, &layout, config)
60    }
61
62    /// Render a window of rows (for scrolling/pagination) using a pre-computed layout.
63    ///
64    /// - `skip_rows`: Number of rows to skip from the beginning
65    /// - `height`: Stop after rendering height number of lines
66    ///
67    /// The header is always included in the output.
68    pub fn render_window(
69        self,
70        layout: &layout::Layout,
71        config: &Config,
72        skip_rows: usize,
73        height: usize,
74    ) -> (String, usize) {
75        render::render_window(self, layout, config, skip_rows, height)
76    }
77
78    fn ty(&self) -> &'t ir::Ty {
79        self.reader.ty()
80    }
81
82    fn remaining(&self) -> usize {
83        self.reader.remaining()
84    }
85
86    /// Resolve type identifiers to their definitions.
87    pub(super) fn get_ty_mat<'a>(&self, ty: &'a ir::Ty) -> &'a ir::Ty
88    where
89        't: 'a,
90    {
91        self.reader.get_ty_mat(ty)
92    }
93
94    /// Returns the row item type (unwraps array if root is array).
95    pub(super) fn row_ty(&self) -> &'t ir::Ty {
96        let ty = self.get_ty_mat(self.ty());
97        match &ty.kind {
98            ir::TyKind::Array(item) => item.as_ref(),
99            _ => ty,
100        }
101    }
102
103    /// Returns true if the type can be rendered in a single cell.
104    pub(super) fn is_flat(&self, ty: &ir::Ty) -> bool {
105        match &self.get_ty_mat(ty).kind {
106            ir::TyKind::Primitive(_) => true,
107            ir::TyKind::Enum(variants) => variants.iter().all(|v| {
108                let payload_ty = self.get_ty_mat(&v.ty);
109                match &payload_ty.kind {
110                    ir::TyKind::Tuple(fields) if fields.is_empty() => true,
111                    _ => self.is_flat(&v.ty),
112                }
113            }),
114            ir::TyKind::Tuple(_) => false,
115            ir::TyKind::Array(_) => false,
116            ir::TyKind::Function(_) => false,
117            ir::TyKind::Ident(_) => unreachable!("should be resolved"),
118        }
119    }
120}