Skip to main content

datui_lib/
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 control bar, 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
67pub struct ConfirmationModal {
68    pub active: bool,
69    pub message: String,
70    pub focus_yes: bool, // true = Yes focused, false = No focused
71    /// What Enter-on-Yes does, named: "Overwrite", not a generic "Yes".
72    pub yes_label: &'static str,
73    /// What Enter-on-No does: "No", unless declining does something of its own.
74    pub no_label: &'static str,
75    /// How far a long message is scrolled; the render clamps it.
76    pub scroll: usize,
77}
78
79impl Default for ConfirmationModal {
80    fn default() -> Self {
81        Self {
82            active: false,
83            message: String::new(),
84            focus_yes: true,
85            yes_label: "Yes",
86            no_label: "No",
87            scroll: 0,
88        }
89    }
90}
91
92impl ConfirmationModal {
93    pub fn new() -> Self {
94        Self::default()
95    }
96
97    pub fn show(&mut self, message: String) {
98        self.active = true;
99        self.message = message;
100        self.focus_yes = true; // Default to Yes
101        self.yes_label = "Yes";
102        self.no_label = "No";
103        self.scroll = 0;
104    }
105
106    /// A choice between two things to do, each named; Esc does neither.
107    pub fn show_choice(
108        &mut self,
109        message: String,
110        yes_label: &'static str,
111        no_label: &'static str,
112    ) {
113        self.show(message);
114        self.yes_label = yes_label;
115        self.no_label = no_label;
116    }
117
118    /// A confirmation whose Yes destroys something: it starts on No, so a
119    /// reflexive second Enter declines, and the action is named on the choice.
120    pub fn show_destructive(&mut self, message: String, yes_label: &'static str) {
121        self.active = true;
122        self.message = message;
123        self.focus_yes = false;
124        self.yes_label = yes_label;
125        self.no_label = "No";
126        self.scroll = 0;
127    }
128
129    pub fn hide(&mut self) {
130        self.active = false;
131        self.message.clear();
132        self.focus_yes = true;
133        self.yes_label = "Yes";
134        self.no_label = "No";
135        self.scroll = 0;
136    }
137}