Skip to main content

datui_lib/app/
feedback.rs

1//! What the user is told: the error modal, the completion flash, and the confirmation
2//! modal.
3
4#[derive(Default)]
5pub struct ErrorModal {
6    pub active: bool,
7    pub message: String,
8    /// How far a long message is scrolled; the render clamps it.
9    pub scroll: usize,
10}
11
12impl ErrorModal {
13    pub fn new() -> Self {
14        Self::default()
15    }
16
17    pub fn show(&mut self, message: String) {
18        // Every failure the user is shown, background work's included, so a bug
19        // report has the message after the modal is gone.
20        log::error!(target: "datui", "{message}");
21        self.active = true;
22        self.message = message;
23        self.scroll = 0;
24    }
25
26    pub fn hide(&mut self) {
27        self.active = false;
28        self.message.clear();
29        self.scroll = 0;
30    }
31}
32
33/// A completion flash (the Feedback rules' second rung): one plain sentence on
34/// the footer, cleared by the next keypress or after two seconds,
35/// whichever comes first. Every screen's completions go here, the home screen's
36/// included. The home screen's own status line beside the filter is for what a
37/// key could not do and why, which has to survive until it is read.
38pub struct Flash {
39    pub message: String,
40    /// Where a path the message ends in starts: cut short, the path loses its
41    /// beginning rather than its file name.
42    pub(crate) path_from: Option<usize>,
43    pub(crate) expires: std::time::Instant,
44}
45
46impl Flash {
47    pub(crate) fn new(message: String) -> Self {
48        Self {
49            message,
50            path_from: None,
51            expires: std::time::Instant::now() + std::time::Duration::from_secs(2),
52        }
53    }
54
55    /// `prefix` then `path`, as in `Exported to /data/out.csv`.
56    pub(crate) fn path(prefix: &str, path: &std::path::Path) -> Self {
57        let mut flash = Self::new(format!("{prefix}{}", path.display()));
58        flash.path_from = Some(prefix.len());
59        flash
60    }
61
62    pub(crate) fn expired(&self) -> bool {
63        std::time::Instant::now() >= self.expires
64    }
65}
66
67/// What a confirmation's Yes does. One question is up at a time, so one of these is
68/// armed at a time, and closing the question disarms it.
69pub(crate) enum Confirm {
70    /// Read every row, as the sample.
71    ReadAll,
72    OpenLink(String),
73    ClearRecents,
74    /// Run Setup's full scan, past the question.
75    QualityFullScan,
76    HideExamples,
77    DeleteView(String),
78    ForgetPlace(std::path::PathBuf),
79    /// Overwrite the file the report's export asked about.
80    QualityExport(
81        std::path::PathBuf,
82        crate::analysis::quality_export::ReportFormat,
83    ),
84    ChartExport(Box<crate::chart::chart_export::ChartExportRequest>),
85    Export(Box<crate::ExportRequest>),
86    Copy(crate::clipboard::CopyFormat, bool),
87    /// Download a remote file, or read a large one whole: the open in flight asks.
88    Download,
89    /// Read the dataset again, where it was (or where the failed read was going): a
90    /// file it listed is gone.
91    Reopen(Option<Box<crate::loading::open_options::KeptPlace>>),
92    /// Stop a recording or keep it, on the way out; either choice leaves.
93    Leave(crate::Leaving),
94}
95
96pub struct ConfirmationModal {
97    pub active: bool,
98    pub message: String,
99    pub focus_yes: bool, // true = Yes focused, false = No focused
100    /// What Enter-on-Yes does, named: "Overwrite", not a generic "Yes".
101    pub yes_label: &'static str,
102    /// What Enter-on-No does: "No", unless declining does something of its own.
103    pub no_label: &'static str,
104    /// How far a long message is scrolled; the render clamps it.
105    pub scroll: usize,
106    /// What is being asked about.
107    pub(crate) asking: Option<Confirm>,
108}
109
110impl Default for ConfirmationModal {
111    fn default() -> Self {
112        Self {
113            active: false,
114            message: String::new(),
115            focus_yes: true,
116            yes_label: "Yes",
117            no_label: "No",
118            scroll: 0,
119            asking: None,
120        }
121    }
122}
123
124impl ConfirmationModal {
125    pub fn new() -> Self {
126        Self::default()
127    }
128
129    pub(crate) fn show(&mut self, message: String, asking: Confirm) {
130        self.active = true;
131        self.message = message;
132        self.focus_yes = true; // Default to Yes
133        self.yes_label = "Yes";
134        self.no_label = "No";
135        self.scroll = 0;
136        self.asking = Some(asking);
137    }
138
139    /// A choice between two things to do, each named; Esc does neither.
140    pub(crate) fn show_choice(
141        &mut self,
142        message: String,
143        yes_label: &'static str,
144        no_label: &'static str,
145        asking: Confirm,
146    ) {
147        self.show(message, asking);
148        self.yes_label = yes_label;
149        self.no_label = no_label;
150    }
151
152    /// A confirmation whose Yes destroys something: it starts on No, so a
153    /// reflexive second Enter declines, and the action is named on the choice.
154    pub(crate) fn show_destructive(
155        &mut self,
156        message: String,
157        yes_label: &'static str,
158        asking: Confirm,
159    ) {
160        self.show(message, asking);
161        self.focus_yes = false;
162        self.yes_label = yes_label;
163    }
164
165    /// Close the question, and hand back what it was asking about.
166    pub(crate) fn take(&mut self) -> Option<Confirm> {
167        let asking = self.asking.take();
168        self.hide();
169        asking
170    }
171
172    /// Whether the question up is the full-scan one Setup's Run asks.
173    pub fn asks_full_scan(&self) -> bool {
174        self.active && matches!(self.asking, Some(Confirm::QualityFullScan))
175    }
176
177    pub fn hide(&mut self) {
178        self.active = false;
179        self.message.clear();
180        self.focus_yes = true;
181        self.yes_label = "Yes";
182        self.no_label = "No";
183        self.scroll = 0;
184        self.asking = None;
185    }
186}