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 {
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::rc::Rc<std::cell::RefCell<Vec<Notice>>>);
69
70impl Log {
71    /// Everything said since the last `clear`, oldest first.
72    pub fn notices(&self) -> Vec<Notice> {
73        self.0.borrow().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.borrow().last().map(Notice::text).unwrap_or_default()
79    }
80
81    /// Whether anything said matches; the usual test question.
82    pub fn said(&self, needle: &str) -> bool {
83        self.0.borrow().iter().any(|n| n.text().contains(needle))
84    }
85}
86
87impl NoticeSink for Log {
88    fn notice(&mut self, notice: Notice) {
89        self.0.borrow_mut().push(notice);
90    }
91
92    fn latest(&self) -> Option<&Notice> {
93        // A `RefCell` cannot hand out a plain reference; a caller
94        // that wants the last notice asks for its text.
95        None
96    }
97
98    fn clear(&mut self) {
99        self.0.borrow_mut().clear();
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn synced_reads_as_prose() {
109        let notice = Notice::Synced {
110            deleted: 2,
111            updated: 3,
112        };
113        assert_eq!(notice.text(), "synced: 2 deleted, 3 updated");
114        assert!(!notice.is_error());
115    }
116
117    #[test]
118    fn new_mail_is_prose_a_bell_can_recognize() {
119        let notice = Notice::NewMail("new mail in inbox (+2)".into());
120        assert_eq!(notice.text(), "new mail in inbox (+2)");
121        assert!(notice.is_new_mail() && !notice.is_error());
122        assert!(!Notice::Info("x".into()).is_new_mail());
123    }
124
125    #[test]
126    fn a_log_keeps_the_order_and_a_handle_reads_it() {
127        let log = Log::default();
128        let mut handle = log.clone();
129        handle.notice(Notice::Info("first".into()));
130        handle.notice(Notice::Error("second".into()));
131        assert_eq!(log.notices().len(), 2);
132        assert_eq!(log.last_text(), "second");
133        assert!(log.said("fir"));
134        handle.clear();
135        assert_eq!(log.notices(), vec![]);
136    }
137}