Skip to main content

rmut_core/
notice.rs

1//! What an operation has to say for itself.
2//!
3//! Operations used to write their outcome into the TUI's status
4//! field, which made every one of them a piece of the TUI. A notice
5//! is that outcome as a value: the operation emits it, and whoever
6//! installed the sink decides what it looks like. A terminal draws it
7//! on the message line; a test reads it.
8
9/// One thing worth telling the user, in the operation's own terms.
10///
11/// `Info` and `Error` carry prose, which is all most outcomes are.
12/// A variant earns its own shape when something other than a person
13/// wants to read it: a test asserting on what a sync did should not
14/// have to parse a sentence.
15#[derive(Debug, Clone, PartialEq, Eq)]
16pub enum Notice {
17    Info(String),
18    Error(String),
19    /// A mailbox sync that got through: messages purged, and flag
20    /// changes written back.
21    Synced {
22        deleted: usize,
23        updated: usize,
24    },
25    /// Mail has arrived, where the prose says. A front end asked for
26    /// mutt's $beep_new needs to know that without reading the
27    /// sentence, which is what earns this its own shape.
28    NewMail(String),
29}
30
31impl Notice {
32    /// Whether this is bad news: the front end colours it, and rings
33    /// the bell if the user asked for one.
34    pub fn is_error(&self) -> bool {
35        matches!(self, Notice::Error(_))
36    }
37
38    /// Whether mail has just arrived: $beep_new rings for it.
39    pub fn is_new_mail(&self) -> bool {
40        matches!(self, Notice::NewMail(_))
41    }
42
43    /// The prose for a front end with a line to spare.
44    pub fn text(&self) -> String {
45        match self {
46            Notice::Info(msg) | Notice::Error(msg) | Notice::NewMail(msg) => msg.clone(),
47            Notice::Synced { deleted, updated } => {
48                format!("synced: {deleted} deleted, {updated} updated")
49            }
50        }
51    }
52}
53
54/// Where notices go. The front end installs one and reads it back
55/// however it likes.
56pub trait NoticeSink: Send {
57    fn notice(&mut self, notice: Notice);
58    /// The last notice, or nothing since the last `clear`.
59    fn latest(&self) -> Option<&Notice>;
60    /// Forget it: a new key means the old message has been read.
61    fn clear(&mut self);
62}
63
64/// The sink a test wants: every notice, in order, behind a handle, so
65/// the test can read what was said while whatever it is driving holds
66/// a handle of its own.
67#[derive(Debug, Clone, Default)]
68pub struct Log(std::sync::Arc<std::sync::Mutex<Vec<Notice>>>);
69
70impl Log {
71    /// Everything said since the last `clear`, oldest first.
72    pub fn notices(&self) -> Vec<Notice> {
73        self.0.lock().unwrap().clone()
74    }
75
76    /// The prose of the last thing said, empty when nothing was.
77    pub fn last_text(&self) -> String {
78        self.0
79            .lock()
80            .unwrap()
81            .last()
82            .map(Notice::text)
83            .unwrap_or_default()
84    }
85
86    /// Whether anything said matches; the usual test question.
87    pub fn said(&self, needle: &str) -> bool {
88        self.0
89            .lock()
90            .unwrap()
91            .iter()
92            .any(|n| n.text().contains(needle))
93    }
94}
95
96impl NoticeSink for Log {
97    fn notice(&mut self, notice: Notice) {
98        self.0.lock().unwrap().push(notice);
99    }
100
101    fn latest(&self) -> Option<&Notice> {
102        // A lock cannot hand out a plain reference; a caller that
103        // wants the last notice asks for its text.
104        None
105    }
106
107    fn clear(&mut self) {
108        self.0.lock().unwrap().clear();
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[test]
117    fn synced_reads_as_prose() {
118        let notice = Notice::Synced {
119            deleted: 2,
120            updated: 3,
121        };
122        assert_eq!(notice.text(), "synced: 2 deleted, 3 updated");
123        assert!(!notice.is_error());
124    }
125
126    #[test]
127    fn new_mail_is_prose_a_bell_can_recognize() {
128        let notice = Notice::NewMail("new mail in inbox (+2)".into());
129        assert_eq!(notice.text(), "new mail in inbox (+2)");
130        assert!(notice.is_new_mail() && !notice.is_error());
131        assert!(!Notice::Info("x".into()).is_new_mail());
132    }
133
134    #[test]
135    fn a_log_keeps_the_order_and_a_handle_reads_it() {
136        let log = Log::default();
137        let mut handle = log.clone();
138        handle.notice(Notice::Info("first".into()));
139        handle.notice(Notice::Error("second".into()));
140        assert_eq!(log.notices().len(), 2);
141        assert_eq!(log.last_text(), "second");
142        assert!(log.said("fir"));
143        handle.clear();
144        assert_eq!(log.notices(), vec![]);
145    }
146}