Skip to main content

datui_lib/
text_formats.rs

1//! Text formats read into a table of their own: VCD value change dumps, FIX logs and
2//! SDF compound files, beside the GPS logs of [`crate::gps`].
3//!
4//! None can be scanned in place, so an open reads the file once, start to end, a piece
5//! at a time, and writes its rows to temporary Arrow IPC segments (see
6//! [`crate::segments`]) that the dataset scans lazily. Memory stays at one batch
7//! however long the file. What a file says besides its rows (a VCD header, the FIX
8//! dictionaries used, an SDF file's fields) is a [`Detail`] for the Info panel.
9
10use std::io::Read;
11use std::sync::Arc;
12
13use color_eyre::Result;
14use color_eyre::eyre::eyre;
15
16use crate::model_files::MetaValue;
17use crate::notes::Note;
18use crate::numfmt::group_chrome;
19use crate::readers::{ConvertIn, ConvertOut};
20use crate::segments::Converted;
21
22/// How much of the file is read at a time.
23const CHUNK: usize = 1 << 16;
24/// The most rows of a [`Detail`] list; a file of more says how many are left out.
25pub const MAX_DETAIL_ROWS: usize = 10_000;
26
27/// What a file says besides its rows, for its tab of the Info panel: a VCD header, the
28/// FIX dictionaries used, a model's totals and metadata, an audio file's format and
29/// markers. Every format's tab is one of these, made by its reader when it opens.
30#[derive(Debug, Clone, Default, PartialEq)]
31pub struct Detail {
32    /// The tab's name: `VCD`, `Model`, `Audio`, as [`tab`] gives it.
33    pub tab: &'static str,
34    /// The lines above the list.
35    pub lines: Vec<String>,
36    /// Lines after them in the warning color: what the file holds that is not shown.
37    pub warnings: Vec<String>,
38    /// The list's title: `Signals`, `Metadata`, `Tracks`.
39    pub list_title: &'static str,
40    /// The list, key and value.
41    pub list: Vec<(String, MetaValue)>,
42    /// Whether `i` opens on this tab rather than the schema: a table whose columns are
43    /// the same for every file, where what is particular to it is here.
44    pub first: bool,
45    /// Whether the columns are datui's own rather than the file's, as the Schema tab
46    /// says of where they came from.
47    pub own_columns: bool,
48    /// The keys of `list` that are tables of the file, which `T` at the table and
49    /// Enter on the tab open: a workbook's worksheets, a database's tables.
50    pub tables: Vec<String>,
51    /// The one of `tables` on screen.
52    pub table: Option<String>,
53}
54
55/// The name of `format`'s tab of the Info panel, as its descriptor says it: a reader
56/// names its [`Detail`] by this, so the descriptor is the one place that says it.
57pub const fn tab(format: crate::FileFormat) -> &'static str {
58    match format.descriptor().summary {
59        crate::Summary::Tab(tab) => tab,
60        // A format said to have no tab has none to name; its title stands in.
61        crate::Summary::None(_) => format.descriptor().title,
62    }
63}
64
65/// Read the one file of `input` a piece at a time with `read`, a text format's reader,
66/// through its compression: the conversion of a VCD dump, FIX log or SDF file.
67pub(crate) fn read_one(
68    input: &ConvertIn<'_>,
69    read: impl FnOnce(&mut Pieces<'_>) -> Result<(Converted, Detail)>,
70) -> ConvertOut {
71    let [file] = input.files else {
72        return Err(eyre!("Open {} files one at a time.", input.format.name()));
73    };
74    let writer = input.writer;
75    let mut reader = crate::gps::open_reader(file, input.options, input.read)?;
76    let mut pieces = |each: &mut dyn FnMut(&[u8]) -> Result<()>| -> Result<()> {
77        let mut chunk = vec![0u8; CHUNK];
78        loop {
79            if writer.stopped() {
80                return Err(eyre!("Reading was stopped."));
81            }
82            let n = match reader.read(&mut chunk) {
83                Ok(0) => return Ok(()),
84                Ok(n) => n,
85                Err(e) if e.kind() == std::io::ErrorKind::Interrupted => continue,
86                Err(e) => return Err(e.into()),
87            };
88            each(&chunk[..n])?;
89        }
90    };
91    let (converted, detail) = read(&mut pieces)?;
92    Ok((converted, Some(Arc::new(detail))))
93}
94
95/// Feeds a reader the file a piece at a time; returns when the file ends.
96pub(crate) type Pieces<'a> = dyn FnMut(&mut dyn FnMut(&[u8]) -> Result<()>) -> Result<()> + 'a;
97
98pub(crate) fn note(summary: String, scope: String) -> Note {
99    Note {
100        summary,
101        scope,
102        read_as_text: None,
103        passed_over: None,
104    }
105}
106
107/// `n` and its noun, singular or plural: `1 line`, `2 lines`.
108pub(crate) fn count(n: u64, one: &str, many: &str) -> String {
109    let n = usize::try_from(n).unwrap_or(usize::MAX);
110    format!("{} {}", group_chrome(n), if n == 1 { one } else { many })
111}
112
113/// A list for a [`Detail`], at most [`MAX_DETAIL_ROWS`] long; one row more says how
114/// many were left out.
115pub(crate) fn capped_list(
116    rows: impl Iterator<Item = (String, MetaValue)>,
117    total: usize,
118) -> Vec<(String, MetaValue)> {
119    let mut list: Vec<_> = rows.take(MAX_DETAIL_ROWS).collect();
120    if total > list.len() {
121        let more = total - list.len();
122        list.push((
123            crate::glyphs::get().ellipsis.to_string(),
124            MetaValue::Text(format!("{} more", group_chrome(more))),
125        ));
126    }
127    list
128}