Skip to main content

datui_lib/
table_switch.rs

1//! `T` at the table: the other tables of the source on screen, to open one in its
2//! place. Listed from what the open already holds (a workbook's or a database's tab of
3//! the Info panel, a spec's record types, a cache's splits), so nothing is read to list
4//! them; opening one is an open of the file with `--table`, as home's row for it is.
5
6use std::path::PathBuf;
7
8use crate::OpenOptions;
9use crate::widgets::datatable::DataTableState;
10
11/// One table of the source, as the picker lists it.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Table {
14    /// What `--table` names; `None` for the whole file (every record type).
15    pub table: Option<String>,
16    /// The picker's line.
17    pub label: String,
18    /// What is cheap to say of it: a sheet's range and size, a table's columns.
19    pub detail: String,
20}
21
22/// The tables of the source on screen.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub struct Tables {
25    /// The file (or a cache's directory) the tables are in.
26    pub file: PathBuf,
27    pub tables: Vec<Table>,
28    /// The one on screen.
29    pub current: Option<usize>,
30}
31
32impl Tables {
33    /// Whether there is another to switch to.
34    pub fn several(&self) -> bool {
35        self.tables.len() > 1
36    }
37}
38
39/// The tables of the dataset `state` opened from `paths` with `options`: `None` for a
40/// source of one table, or several files.
41pub fn of(state: &DataTableState, paths: &[PathBuf], options: &OpenOptions) -> Option<Tables> {
42    let [file] = paths else {
43        return None;
44    };
45    let file = file.clone();
46    if let Some(detail) = state.format_detail()
47        && !detail.tables.is_empty()
48    {
49        return Some(from_detail(file, detail, options.table.as_deref()));
50    }
51    if let Some(read) = state.format_read()
52        && !read.spec.is_delimited()
53        && read.spec.records.variants.len() > 1
54    {
55        return Some(record_types(file, &read.spec));
56    }
57    let splits = options.splits.as_deref()?;
58    let current = splits.split.clone()?;
59    if splits.others.is_empty() {
60        return None;
61    }
62    let mut names: Vec<String> = splits.others.clone();
63    names.push(current.clone());
64    names.sort();
65    names.dedup();
66    let at = names.iter().position(|n| *n == current);
67    Some(Tables {
68        file,
69        tables: names
70            .into_iter()
71            .map(|name| Table {
72                table: Some(name.clone()),
73                label: name,
74                detail: "split".to_string(),
75            })
76            .collect(),
77        current: at,
78    })
79}
80
81/// Whether [`of`] would list more than one table, without listing them: asked by the
82/// footer each frame.
83pub fn several(state: &DataTableState, paths: &[PathBuf], options: &OpenOptions) -> bool {
84    if paths.len() != 1 {
85        return false;
86    }
87    if let Some(detail) = state.format_detail()
88        && !detail.tables.is_empty()
89    {
90        return detail.tables.len() > 1;
91    }
92    if let Some(read) = state.format_read()
93        && !read.spec.is_delimited()
94        && read.spec.records.variants.len() > 1
95    {
96        return true;
97    }
98    options
99        .splits
100        .as_deref()
101        .is_some_and(|s| s.split.is_some() && s.others.iter().any(|o| Some(o) != s.split.as_ref()))
102}
103
104/// A workbook's worksheets or a database's tables, as its tab of the Info panel lists
105/// them: what that says of each, less that it is the one opened, which the picker marks.
106fn from_detail(file: PathBuf, detail: &crate::text_formats::Detail, asked: Option<&str>) -> Tables {
107    let said = |name: &str| {
108        detail
109            .list
110            .iter()
111            .find(|(key, _)| key == name)
112            .map(|(_, value)| match value {
113                crate::model_files::MetaValue::Text(text) => text
114                    .split(", ")
115                    .filter(|part| *part != "opened")
116                    .collect::<Vec<_>>()
117                    .join(", "),
118                _ => String::new(),
119            })
120            .unwrap_or_default()
121    };
122    let opened = detail.table.as_deref().or(asked);
123    let current = opened.and_then(|name| detail.tables.iter().position(|t| t == name));
124    Tables {
125        file,
126        tables: detail
127            .tables
128            .iter()
129            .map(|name| Table {
130                table: Some(name.clone()),
131                label: name.clone(),
132                detail: said(name),
133            })
134            .collect(),
135        current,
136    }
137}
138
139/// A format spec's record types, after the whole file (every type, a `type` column
140/// saying which).
141fn record_types(file: PathBuf, spec: &crate::formats::Spec) -> Tables {
142    let current = spec.variant.as_deref();
143    let whole = Table {
144        table: None,
145        label: file
146            .file_name()
147            .map(|n| n.to_string_lossy().into_owned())
148            .unwrap_or_else(|| file.display().to_string()),
149        detail: "every record type".to_string(),
150    };
151    let types = crate::members::variant_tables(spec)
152        .into_iter()
153        .map(|t| Table {
154            detail: crate::text_formats::count(t.columns.len() as u64, "column", "columns"),
155            table: Some(t.name.clone()),
156            label: t.name,
157        });
158    let tables: Vec<Table> = std::iter::once(whole).chain(types).collect();
159    let at = tables.iter().position(|t| t.table.as_deref() == current);
160    Tables {
161        file,
162        tables,
163        current: at,
164    }
165}