Skip to main content

datui_lib/home/
home_preview.rs

1//! The home screen's `ROWS` preview: the first rows of the selected file, read the way
2//! the open reads them and handed to the open as its first page (#547 M4).
3//!
4//! File reads are not spent twice. The worker runs the open's own scan and schema read
5//! and fills the page the table would ask for first; the pane shows a few rows of it,
6//! and Enter installs the dataset it built. The open then has nothing left to read
7//! for that page, and no scan or schema read of its own.
8
9use crate::FileFormat;
10use crate::home::discover::{Entry, EntryKind};
11use crate::table::DataTableState;
12use polars::prelude::{DataFrame, DataType};
13use std::collections::{HashMap, VecDeque};
14use std::path::{Path, PathBuf};
15use std::sync::{Arc, Mutex};
16use std::time::SystemTime;
17
18/// Rows the pane shows.
19pub const PREVIEW_ROWS: usize = 8;
20/// Columns kept for the pane. More than a wide pane shows; the rest are counted.
21const PREVIEW_COLUMNS: usize = 24;
22/// Previews kept for the session, newest last.
23const KEPT: usize = 32;
24/// A cell longer than this is cut when it is kept: the pane never shows more.
25const CELL_MAX: usize = 40;
26
27/// What a file was when it was read: its size and modification time. A file that
28/// changed since is read again, and its old page is never installed.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub struct Stamp {
31    pub len: Option<u64>,
32    pub modified: Option<SystemTime>,
33}
34
35impl Stamp {
36    /// From what the listing already knows of a row, so nothing is stat'ed to draw.
37    pub fn of_entry(entry: &Entry) -> Self {
38        Self {
39            len: entry.size,
40            modified: entry.modified,
41        }
42    }
43
44    /// From the file as it is now.
45    pub fn of_file(path: &Path) -> Option<Self> {
46        let meta = std::fs::metadata(path).ok()?;
47        Some(Self {
48            len: Some(meta.len()),
49            modified: meta.modified().ok(),
50        })
51    }
52}
53
54/// The first rows of a file, as text, for the pane: the leading columns and how many
55/// there are in all.
56#[derive(Debug, Clone, PartialEq)]
57pub struct PreviewRows {
58    /// The leading columns, named and typed.
59    pub columns: Vec<(String, DataType)>,
60    /// Each row's cells, `None` for a null.
61    pub rows: Vec<Vec<Option<String>>>,
62    /// Columns in the dataset, the ones not kept included.
63    pub total_columns: usize,
64}
65
66impl PreviewRows {
67    /// The first [`PREVIEW_ROWS`] rows of `df`, its leading columns only.
68    pub fn from_frame(df: &DataFrame) -> Self {
69        let columns: Vec<_> = df
70            .columns()
71            .iter()
72            .filter(|c| c.name().as_str() != crate::formats::schema_union::DRIFT_COLUMN)
73            .collect();
74        let total_columns = columns.len();
75        let kept: Vec<_> = columns.into_iter().take(PREVIEW_COLUMNS).collect();
76        let height = df.height().min(PREVIEW_ROWS);
77        let rows = (0..height)
78            .map(|i| {
79                kept.iter()
80                    .map(|c| match c.get(i) {
81                        Ok(polars::prelude::AnyValue::Null) | Err(_) => None,
82                        Ok(value) => {
83                            let text = crate::exact::str_value(&value);
84                            // One line: a newline inside a cell would break the row.
85                            let line: String = text
86                                .chars()
87                                .map(|ch| if ch.is_control() { ' ' } else { ch })
88                                .take(CELL_MAX)
89                                .collect();
90                            Some(line)
91                        }
92                    })
93                    .collect()
94            })
95            .collect();
96        Self {
97            columns: kept
98                .iter()
99                .map(|c| (c.name().to_string(), c.dtype().clone()))
100                .collect(),
101            rows,
102            total_columns,
103        }
104    }
105}
106
107/// A dataset built and its first page read, waiting for the open that will show it.
108pub struct Prepared {
109    pub(crate) state: Box<DataTableState>,
110    pub(crate) options: crate::OpenOptions,
111    pub(crate) debug_label: Option<String>,
112    pub(crate) progress: Arc<crate::formats::schema_union::FooterProgress>,
113}
114
115/// A prepared dataset passed from the preview to the open that takes it, once.
116pub type Handoff = Arc<Mutex<Option<Box<Prepared>>>>;
117
118/// The previews read this session, and the one dataset waiting to be opened.
119#[derive(Default)]
120pub struct Previews {
121    /// What was read for each file, at the stamp it was read at. `None` for a file
122    /// that could not be previewed, so it is not asked about again.
123    rows: HashMap<PathBuf, (Stamp, Option<Arc<PreviewRows>>)>,
124    order: VecDeque<PathBuf>,
125    /// The newest preview's dataset. Only one is kept: an open is of the row the
126    /// cursor is on, and a dataset per row passed over would hold their pages.
127    prepared: Option<(PathBuf, Stamp, Box<Prepared>)>,
128    /// The file being read. One at a time: a cursor run down a directory reads the
129    /// row it rests on, not every row it passed.
130    pub(crate) inflight: Option<PathBuf>,
131}
132
133/// The reads of data the app has started, by kind: what a test counts to show a read
134/// was not made twice.
135#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
136pub struct ReadCounts {
137    /// Previews read for the home screen.
138    pub previews: usize,
139    /// Opens that scanned their paths.
140    pub scans: usize,
141    /// Pages of rows read for a dataset on screen.
142    pub pages: usize,
143}
144
145impl Previews {
146    /// What is known of `path` at `stamp`: `Some(None)` when it cannot be previewed.
147    pub fn rows(&self, path: &Path, stamp: Stamp) -> Option<Option<Arc<PreviewRows>>> {
148        self.rows
149            .get(path)
150            .filter(|(at, _)| *at == stamp)
151            .map(|(_, rows)| rows.clone())
152    }
153
154    /// Whether `path` is being read now.
155    pub fn reading(&self, path: &Path) -> bool {
156        self.inflight.as_deref() == Some(path)
157    }
158
159    /// Keep what a worker read.
160    /// `stamp` is the row's, which the rows are kept under; `read_at` the file's when
161    /// it was read, which the dataset is installed under.
162    pub(crate) fn landed(
163        &mut self,
164        path: PathBuf,
165        stamp: Stamp,
166        read_at: Stamp,
167        rows: Option<Arc<PreviewRows>>,
168        prepared: Option<Box<Prepared>>,
169    ) {
170        if self.inflight.as_ref() == Some(&path) {
171            self.inflight = None;
172        }
173        if let Some(prepared) = prepared {
174            self.prepared = Some((path.clone(), read_at, prepared));
175        }
176        self.order.retain(|p| p != &path);
177        self.order.push_back(path.clone());
178        self.rows.insert(path, (stamp, rows));
179        while self.order.len() > KEPT {
180            if let Some(old) = self.order.pop_front() {
181                self.rows.remove(&old);
182            }
183        }
184    }
185
186    /// The dataset prepared for `path`, if the file is still what was read.
187    pub(crate) fn take_prepared(&mut self, path: &Path) -> Option<Box<Prepared>> {
188        if self.prepared.as_ref()?.0 != path {
189            return None;
190        }
191        let now = Stamp::of_file(path);
192        let (_, stamp, prepared) = self.prepared.take()?;
193        (now == Some(stamp)).then_some(prepared)
194    }
195
196    /// Let go of the dataset waiting to be opened: the app is leaving home.
197    pub(crate) fn drop_prepared(&mut self) {
198        self.prepared = None;
199    }
200}
201
202/// Whether a row's first rows are worth reading before it is opened: a local file of a
203/// format whose first page is cheap, under `max_bytes` (Parquet: its average row
204/// group). Nothing on a network share or in a store is read before it is opened.
205pub fn previewable(entry: &Entry, max_bytes: u64) -> bool {
206    if max_bytes == 0
207        || entry.kind != EntryKind::File
208        || entry.opens_whole_directory
209        || entry.table.is_some()
210        || entry.format_spec.is_some()
211        || crate::home::is_remote_path(&entry.path)
212    {
213        return false;
214    }
215    let Some(size) = entry.size else {
216        return false;
217    };
218    let preview =
219        FileFormat::from_path(&entry.path).and_then(|f| crate::formats::readers::of(f).preview);
220    match preview {
221        // The first page is the first row group, whatever the file.
222        Some(crate::formats::readers::Preview::RowGroup) => {
223            let groups = entry.cost.row_groups.unwrap_or(1).max(1) as u64;
224            size / groups <= max_bytes
225        }
226        Some(crate::formats::readers::Preview::Scan) => size <= max_bytes,
227        None => false,
228    }
229}