Skip to main content

datui_lib/app/
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::table::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(
107    file: PathBuf,
108    detail: &crate::formats::text_formats::Detail,
109    asked: Option<&str>,
110) -> Tables {
111    let said = |name: &str| {
112        detail
113            .list
114            .iter()
115            .find(|(key, _)| key == name)
116            .map(|(_, value)| match value {
117                crate::formats::model_files::MetaValue::Text(text) => text
118                    .split(", ")
119                    .filter(|part| *part != "opened")
120                    .collect::<Vec<_>>()
121                    .join(", "),
122                _ => String::new(),
123            })
124            .unwrap_or_default()
125    };
126    let opened = detail.table.as_deref().or(asked);
127    let current = opened.and_then(|name| detail.tables.iter().position(|t| t == name));
128    Tables {
129        file,
130        tables: detail
131            .tables
132            .iter()
133            .map(|name| Table {
134                table: Some(name.clone()),
135                label: name.clone(),
136                detail: said(name),
137            })
138            .collect(),
139        current,
140    }
141}
142
143/// A format spec's record types, after the whole file (every type, a `type` column
144/// saying which).
145fn record_types(file: PathBuf, spec: &crate::formats::Spec) -> Tables {
146    let current = spec.variant.as_deref();
147    let whole = Table {
148        table: None,
149        label: file
150            .file_name()
151            .map(|n| n.to_string_lossy().into_owned())
152            .unwrap_or_else(|| file.display().to_string()),
153        detail: "every record type".to_string(),
154    };
155    let types = crate::formats::members::variant_tables(spec)
156        .into_iter()
157        .map(|t| Table {
158            detail: crate::formats::text_formats::count(
159                t.columns.len() as u64,
160                "column",
161                "columns",
162            ),
163            table: Some(t.name.clone()),
164            label: t.name,
165        });
166    let tables: Vec<Table> = std::iter::once(whole).chain(types).collect();
167    let at = tables.iter().position(|t| t.table.as_deref() == current);
168    Tables {
169        file,
170        tables,
171        current: at,
172    }
173}