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}