Skip to main content

rmut_session/
function.rs

1//! What the index can be told to do, and the doing of it.
2//!
3//! mutt names every operation (`<delete-message>`, `<group-reply>`)
4//! and binds keys to the names, which is what makes a keymap a
5//! configuration rather than a program. rmut had the names, but they
6//! lived in the terminal front end next to the dispatch, so the only
7//! way to reach `delete` was to press a key on a pty: 300 lines of
8//! mailbox logic that no test could name.
9//!
10//! The names and the dispatch live here now. A front end resolves
11//! whatever it has (a keystroke, a menu item, `:exec`) to a
12//! [`Function`], hands it to [`Session::run_function`], and reads the
13//! [`Outcome`]. Most functions finish inside the session; some stop
14//! to [`Ask`]; the rest come back as a [`FrontOp`], which is the
15//! honest list of what a session cannot do for itself because it owns
16//! neither a screen nor an editor.
17
18use crate::ask::{Ask, PatternOp};
19use crate::{ComposeKind, Session, ThreadOp};
20
21/// One thing the index can be told to do, under the name mutt gives
22/// it. A front end binds keys or menu items to these; `bind` and
23/// `:exec` name them straight out.
24#[derive(Clone, Copy, PartialEq, Eq, Debug)]
25pub enum Function {
26    Quit,
27    Abort,
28    Down,
29    Up,
30    PageDown,
31    PageUp,
32    First,
33    Last,
34    View,
35    Delete,
36    Undelete,
37    Flag,
38    ToggleNew,
39    Sync,
40    Compose,
41    Reply,
42    GroupReply,
43    ListReply,
44    Forward,
45    Sort,
46    Limit,
47    Search,
48    SearchReverse,
49    SearchNext,
50    NextNew,
51    PrevNew,
52    ChangeMailbox,
53    ChangeMailboxReadOnly,
54    Folders,
55    Attachments,
56    FoldThread,
57    FoldAll,
58    Print,
59    Tag,
60    TagPrefix,
61    DeleteThread,
62    UndeleteThread,
63    TagThread,
64    DeleteSubthread,
65    UndeleteSubthread,
66    NextThread,
67    PrevThread,
68    BreakThread,
69    LinkThreads,
70    ReadThread,
71    ReadSubthread,
72    TagSubthread,
73    ParentMessage,
74    RootMessage,
75    EditLabel,
76    ShowVersion,
77    ShowLimit,
78    ToggleWrite,
79    DisplayAddress,
80    PageTop,
81    PageMiddle,
82    PageBottom,
83    Undo,
84    DeletePattern,
85    UndeletePattern,
86    TagPattern,
87    UntagPattern,
88    FetchMail,
89    Save,
90    Copy,
91    DecodeSave,
92    DecodeCopy,
93    Pipe,
94    Bounce,
95    Resend,
96    Edit,
97    SidebarToggle,
98    SidebarNext,
99    SidebarPrev,
100    SidebarOpen,
101    CreateAlias,
102    Query,
103    Notmuch,
104    EnterCommand,
105    Shell,
106    Redraw,
107    Suspend,
108    Help,
109}
110
111impl Function {
112    pub fn name(self) -> &'static str {
113        use Function::*;
114        match self {
115            Quit => "quit",
116            Abort => "abort",
117            Down => "down",
118            Up => "up",
119            PageDown => "page-down",
120            PageUp => "page-up",
121            First => "first",
122            Last => "last",
123            View => "view",
124            Delete => "delete",
125            Undelete => "undelete",
126            Flag => "flag",
127            ToggleNew => "toggle-new",
128            Sync => "sync",
129            Compose => "compose",
130            Reply => "reply",
131            GroupReply => "group-reply",
132            ListReply => "list-reply",
133            Forward => "forward",
134            Sort => "sort",
135            Limit => "limit",
136            Search => "search",
137            SearchReverse => "search-reverse",
138            SearchNext => "search-next",
139            NextNew => "next-new",
140            PrevNew => "previous-new",
141            ChangeMailbox => "change-mailbox",
142            ChangeMailboxReadOnly => "change-mailbox-readonly",
143            Folders => "folders",
144            Attachments => "attachments",
145            FoldThread => "fold-thread",
146            FoldAll => "fold-all",
147            Print => "print",
148            Tag => "tag",
149            TagPrefix => "tag-prefix",
150            DeleteThread => "delete-thread",
151            UndeleteThread => "undelete-thread",
152            TagThread => "tag-thread",
153            DeleteSubthread => "delete-subthread",
154            UndeleteSubthread => "undelete-subthread",
155            NextThread => "next-thread",
156            BreakThread => "break-thread",
157            LinkThreads => "link-threads",
158            ReadThread => "read-thread",
159            ReadSubthread => "read-subthread",
160            TagSubthread => "tag-subthread",
161            ParentMessage => "parent-message",
162            RootMessage => "root-message",
163            EditLabel => "edit-label",
164            ShowVersion => "show-version",
165            ShowLimit => "show-limit",
166            ToggleWrite => "toggle-write",
167            DisplayAddress => "display-address",
168            PageTop => "top-page",
169            PageMiddle => "middle-page",
170            PageBottom => "bottom-page",
171            PrevThread => "previous-thread",
172            Undo => "undo",
173            DeletePattern => "delete-pattern",
174            UndeletePattern => "undelete-pattern",
175            TagPattern => "tag-pattern",
176            UntagPattern => "untag-pattern",
177            FetchMail => "fetch-mail",
178            Save => "save",
179            Copy => "copy",
180            DecodeSave => "decode-save",
181            DecodeCopy => "decode-copy",
182            Pipe => "pipe",
183            Bounce => "bounce",
184            Resend => "resend",
185            Edit => "edit",
186            SidebarToggle => "sidebar-toggle",
187            SidebarNext => "sidebar-next",
188            SidebarPrev => "sidebar-prev",
189            SidebarOpen => "sidebar-open",
190            CreateAlias => "create-alias",
191            Query => "query",
192            Notmuch => "notmuch",
193            EnterCommand => "enter-command",
194            Shell => "shell-escape",
195            Redraw => "refresh",
196            Suspend => "suspend",
197            Help => "help",
198        }
199    }
200
201    pub fn describe(self) -> &'static str {
202        use Function::*;
203        match self {
204            Quit => "quit (writes changes; asks before purging deletions)",
205            Abort => "quit without saving changes",
206            Down => "next message",
207            Up => "previous message",
208            PageDown => "page down",
209            PageUp => "page up",
210            First => "first message",
211            Last => "last message",
212            View => "view message",
213            Delete => "mark for deletion",
214            Undelete => "unmark deletion",
215            Flag => "toggle flagged mark",
216            ToggleNew => "toggle read/unread",
217            Sync => "write changes to the maildir",
218            Compose => "compose a new message",
219            Reply => "reply to sender",
220            GroupReply => "reply to all",
221            ListReply => "reply to the mailing list only",
222            Forward => "forward message",
223            Sort => "choose sort order",
224            Limit => "limit index by pattern",
225            Search => "search messages by pattern (the pager / searches its text)",
226            SearchReverse => "search backwards; n then repeats backwards too",
227            SearchNext => "repeat last search, the way it was going",
228            NextNew => "jump to the next new or unread message",
229            PrevNew => "jump to the previous new or unread message",
230            ChangeMailbox => "open a mailbox by path",
231            ChangeMailboxReadOnly => "open a mailbox read-only (Alt+c)",
232            Folders => "browse nearby mailboxes",
233            Attachments => "list message parts",
234            FoldThread => "fold/unfold current thread",
235            FoldAll => "fold/unfold all threads",
236            Print => "pipe message to the print command",
237            Tag => "toggle the tag on this message",
238            TagPrefix => "apply the next function to tagged messages",
239            DeleteThread => "mark the whole thread for deletion",
240            UndeleteThread => "unmark the whole thread",
241            TagThread => "tag/untag the whole thread",
242            DeleteSubthread => "mark this message and its replies for deletion",
243            UndeleteSubthread => "unmark this message and its replies",
244            NextThread => "jump to the next thread",
245            BreakThread => "break the thread in two at this message",
246            LinkThreads => "link the tagged messages under this one",
247            ReadThread => "mark the whole thread read",
248            ReadSubthread => "mark this message and its replies read",
249            TagSubthread => "tag this message and its replies",
250            ParentMessage => "jump to the parent message",
251            RootMessage => "jump to the thread's root message",
252            EditLabel => "add, change or clear the X-Label",
253            ShowVersion => "show the rmut version",
254            ShowLimit => "show the active limit pattern",
255            ToggleWrite => "toggle the mailbox's read-only state",
256            DisplayAddress => "show the sender's full address",
257            PageTop => "move to the top of the page",
258            PageMiddle => "move to the middle of the page",
259            PageBottom => "move to the bottom of the page",
260            PrevThread => "jump to the previous thread",
261            Undo => "cancel a held send, else undo the last delete/flag/tag/save",
262            DeletePattern => "delete every message matching a pattern",
263            UndeletePattern => "undelete every message matching a pattern",
264            TagPattern => "tag every message matching a pattern",
265            UntagPattern => "untag every message matching a pattern",
266            FetchMail => "check for new mail now",
267            Save => "save (copy + mark deleted) to a mailbox",
268            DecodeSave => "decode-save: the decoded message, original deleted",
269            DecodeCopy => "decode-copy: the decoded message",
270            Copy => "copy to a mailbox (original stays)",
271            Pipe => "pipe raw message to a shell command",
272            Bounce => "bounce (resend) message to new recipients",
273            Resend => "edit the message as a new draft",
274            Edit => "edit the raw message and replace it",
275            SidebarToggle => "show/hide the mailbox sidebar",
276            SidebarNext => "highlight the next sidebar mailbox",
277            SidebarPrev => "highlight the previous sidebar mailbox",
278            SidebarOpen => "open the highlighted sidebar mailbox",
279            CreateAlias => "add the sender to the alias file",
280            Query => "look up addresses with query_command",
281            Notmuch => "notmuch search into a read-only view",
282            EnterCommand => "run a config command (set/bind/macro/color/...)",
283            Shell => "run a shell command",
284            Redraw => "repaint the screen",
285            Suspend => "suspend rmut (fg brings it back)",
286            Help => "this help",
287        }
288    }
289
290    /// Every function, in the order the help screen and a menu bar
291    /// want them.
292    pub fn all() -> &'static [Function] {
293        use Function::*;
294        &[
295            Quit,
296            Abort,
297            Down,
298            Up,
299            PageDown,
300            PageUp,
301            First,
302            Last,
303            View,
304            Delete,
305            Undelete,
306            Flag,
307            ToggleNew,
308            Sync,
309            Compose,
310            Reply,
311            GroupReply,
312            ListReply,
313            Forward,
314            Sort,
315            Limit,
316            Search,
317            SearchReverse,
318            SearchNext,
319            NextNew,
320            PrevNew,
321            ChangeMailbox,
322            ChangeMailboxReadOnly,
323            Folders,
324            Attachments,
325            FoldThread,
326            FoldAll,
327            Print,
328            Tag,
329            TagPrefix,
330            DeleteThread,
331            UndeleteThread,
332            TagThread,
333            DeleteSubthread,
334            UndeleteSubthread,
335            NextThread,
336            PrevThread,
337            BreakThread,
338            LinkThreads,
339            ReadThread,
340            ReadSubthread,
341            TagSubthread,
342            ParentMessage,
343            RootMessage,
344            EditLabel,
345            ShowVersion,
346            ShowLimit,
347            ToggleWrite,
348            DisplayAddress,
349            PageTop,
350            PageMiddle,
351            PageBottom,
352            Undo,
353            DeletePattern,
354            UndeletePattern,
355            TagPattern,
356            UntagPattern,
357            FetchMail,
358            Save,
359            Copy,
360            DecodeSave,
361            DecodeCopy,
362            Pipe,
363            Bounce,
364            Resend,
365            Edit,
366            SidebarToggle,
367            SidebarNext,
368            SidebarPrev,
369            SidebarOpen,
370            CreateAlias,
371            Query,
372            Notmuch,
373            EnterCommand,
374            Shell,
375            Redraw,
376            Suspend,
377            Help,
378        ]
379    }
380
381    pub fn from_name(name: &str) -> Option<Function> {
382        Function::all().iter().copied().find(|a| a.name() == name)
383    }
384
385    /// Which functions `;` (tag-prefix) can hand the tagged set to.
386    /// The rest say so rather than quietly acting on one message:
387    /// resend and edit open a draft or an editor, of which rmut has
388    /// one at a time.
389    pub fn takes_tagged(self) -> bool {
390        use Function::*;
391        matches!(
392            self,
393            Delete
394                | Undelete
395                | Flag
396                | ToggleNew
397                | Tag
398                | Save
399                | Copy
400                | DecodeSave
401                | DecodeCopy
402                | Pipe
403                | Print
404                | Bounce
405                | EditLabel
406        )
407    }
408}
409
410/// What came of running a function.
411pub enum Outcome {
412    /// The session did it. Whatever there was to say went out as a
413    /// [`Notice`](rmut_core::notice::Notice); whatever the front end
414    /// must do next is waiting in [`Session::take_request`].
415    Done,
416    /// It cannot finish without an answer. The front end collects one
417    /// however it likes and hands it back to [`Session::answer`].
418    Ask(Ask),
419    /// Only the front end can do this one.
420    Front(FrontOp),
421}
422
423impl From<Option<Ask>> for Outcome {
424    /// The `ask_*` helpers return `None` when the question does not
425    /// arise (nothing to act on, a read-only mailbox), having already
426    /// said why.
427    fn from(ask: Option<Ask>) -> Outcome {
428        match ask {
429            Some(ask) => Outcome::Ask(ask),
430            None => Outcome::Done,
431        }
432    }
433}
434
435/// The functions a session cannot carry out, because they are about
436/// the display rather than the mail: a menu to open, a screen to
437/// repaint, an editor to hand the terminal to. The session has done
438/// whatever checking it can (a mailbox with unsaved changes will not
439/// be left, an unconfigured query will not be prompted for) before
440/// handing one of these back, so a front end can act on it directly.
441pub enum FrontOp {
442    /// mutt's `x`: leave without writing anything back.
443    Exit,
444    /// Read the selected message, however messages are read.
445    OpenSelected,
446    /// Start a draft. The session asks for the recipients once the
447    /// front end has settled `$recall`.
448    Compose(ComposeKind),
449    /// mutt's resend-message: the selected message as a new draft.
450    Resend,
451    /// mutt's `e`: the selected message's own bytes, in an editor.
452    RawEdit,
453    /// The attachment menu for the selected message.
454    Attachments,
455    /// The folder browser.
456    Folders,
457    /// `$query_command`, which is configured: ask for the terms.
458    Query,
459    /// notmuch, which is not disabled: ask for the query.
460    Notmuch,
461    /// Somewhere else to open; the open mailbox is ready to be left.
462    ChangeMailbox { read_only: bool },
463    /// The `:` command line.
464    CommandPrompt,
465    /// mutt's `;`: the next function applies to the tagged set. There
466    /// are tagged messages, or this would have been a complaint.
467    TagPrefix,
468    /// The mailbox pane.
469    Sidebar(SidebarOp),
470    /// Move the cursor by where it sits on screen, which only the
471    /// front end knows: mutt's H, M and L.
472    PageMove(PageSpot),
473    /// mutt's help screen.
474    Help,
475    /// mutt's Ctrl+L: repaint.
476    Redraw,
477}
478
479/// What to do with the mailbox pane.
480#[derive(Clone, Copy, PartialEq, Eq, Debug)]
481pub enum SidebarOp {
482    Toggle,
483    Next,
484    Prev,
485    Open,
486}
487
488/// Where on the visible page the cursor should land.
489#[derive(Clone, Copy, PartialEq, Eq, Debug)]
490pub enum PageSpot {
491    Top,
492    Middle,
493    Bottom,
494}
495
496impl Session {
497    /// Do one thing, whatever asked for it: a key, a macro replay, a
498    /// `:exec`, a menu item.
499    ///
500    /// `tagged` is mutt's tag-prefix, and is only ever true for a
501    /// function that [takes it](Function::takes_tagged); `page` is how
502    /// many messages the front end is showing at once, which is all
503    /// the geometry the session needs to know.
504    pub fn run_function(&mut self, function: Function, tagged: bool, page: usize) -> Outcome {
505        use Function::*;
506        match function {
507            // ---- motion ----
508            Down => self.select(self.sel.saturating_add(1)),
509            Up => self.select(self.sel.saturating_sub(1)),
510            PageDown => self.select(self.sel.saturating_add(page)),
511            PageUp => self.select(self.sel.saturating_sub(page)),
512            First => self.select(0),
513            Last => self.select(usize::MAX),
514            NextNew => self.jump_new(true),
515            PrevNew => self.jump_new(false),
516            NextThread => self.jump_thread(true),
517            PrevThread => self.jump_thread(false),
518            ParentMessage => self.jump_parent(false),
519            RootMessage => self.jump_parent(true),
520            SearchNext => self.search_next(),
521
522            // ---- marks ----
523            Tag => {
524                if let Some(&i) = self.visible.get(self.sel) {
525                    self.push_undo("tag", &[i]);
526                    self.msgs[i].env.tagged = !self.msgs[i].env.tagged;
527                    self.select(self.sel.saturating_add(1));
528                }
529            }
530            Delete => self.mark_selected(tagged, "delete", |m| m.env.file.flags.deleted = true),
531            Undelete => {
532                self.mark_selected(tagged, "undelete", |m| m.env.file.flags.deleted = false)
533            }
534            Flag => self.mark_selected(tagged, "flag", |m| {
535                m.env.file.flags.flagged = !m.env.file.flags.flagged
536            }),
537            ToggleNew => self.mark_selected(tagged, "toggle read", |m| {
538                m.env.file.flags.seen = !m.env.file.flags.seen;
539                m.env.file.is_new = false;
540            }),
541            Undo => {
542                // A message still inside its $undo_send window is the
543                // most recent thing done, so it is what undo takes
544                // back first.
545                if !self.cancel_send() {
546                    self.undo_last();
547                }
548            }
549
550            // ---- threads ----
551            DeleteThread => self.thread_mark(false, ThreadOp::Delete),
552            UndeleteThread => self.thread_mark(false, ThreadOp::Undelete),
553            TagThread => self.thread_mark(false, ThreadOp::Tag),
554            ReadThread => self.thread_mark(false, ThreadOp::Read),
555            DeleteSubthread => self.thread_mark(true, ThreadOp::Delete),
556            UndeleteSubthread => self.thread_mark(true, ThreadOp::Undelete),
557            TagSubthread => self.thread_mark(true, ThreadOp::Tag),
558            ReadSubthread => self.thread_mark(true, ThreadOp::Read),
559            BreakThread => self.break_thread(),
560            LinkThreads => self.link_threads(),
561            FoldThread => self.toggle_collapse(false),
562            FoldAll => self.toggle_collapse(true),
563
564            // ---- the mailbox ----
565            Sync => {
566                if self.deleted_count() > 0 {
567                    return self.ask_purge(false).into();
568                }
569                self.sync(true);
570            }
571            Quit => return self.leave().into(),
572            FetchMail => {
573                self.check_new_mail();
574                if self.notice().is_none() {
575                    self.note("checked for new mail");
576                }
577            }
578            ToggleWrite => self.toggle_write(),
579            ShowLimit => self.show_limit(),
580            ShowVersion => self.note(concat!("rmut ", env!("CARGO_PKG_VERSION"))),
581            DisplayAddress => {
582                let from = self
583                    .visible
584                    .get(self.sel)
585                    .map(|&i| self.msgs[i].env.from_full.clone());
586                match from {
587                    Some(from) if !from.trim().is_empty() => self.note(from),
588                    _ => self.note("(no From address)"),
589                }
590            }
591
592            // ---- questions ----
593            Limit => return Outcome::Ask(self.ask_limit()),
594            Search => return Outcome::Ask(self.ask_search(false)),
595            SearchReverse => return Outcome::Ask(self.ask_search(true)),
596            Sort => return Outcome::Ask(self.ask_sort()),
597            Shell => return Outcome::Ask(self.ask_shell()),
598            DeletePattern => return self.ask_pattern(PatternOp::Delete).into(),
599            UndeletePattern => return self.ask_pattern(PatternOp::Undelete).into(),
600            TagPattern => return self.ask_pattern(PatternOp::Tag).into(),
601            UntagPattern => return self.ask_pattern(PatternOp::Untag).into(),
602            Save => return self.ask_copy(true, tagged).into(),
603            Copy => return self.ask_copy(false, tagged).into(),
604            DecodeSave => return self.ask_copy_decode(true, tagged, true).into(),
605            DecodeCopy => return self.ask_copy_decode(false, tagged, true).into(),
606            Pipe => return self.ask_pipe(tagged).into(),
607            Bounce => return self.ask_bounce(tagged).into(),
608            Print => return self.ask_print(tagged).into(),
609            EditLabel => return self.ask_edit_label(tagged).into(),
610            CreateAlias => return self.ask_alias().into(),
611            ListReply => return self.start_list_reply().into(),
612
613            // ---- off to the front end ----
614            Suspend => self.request_suspend(),
615            Abort => return Outcome::Front(FrontOp::Exit),
616            View => return Outcome::Front(FrontOp::OpenSelected),
617            Compose => return Outcome::Front(FrontOp::Compose(ComposeKind::New)),
618            Reply => return Outcome::Front(FrontOp::Compose(ComposeKind::Reply)),
619            GroupReply => return Outcome::Front(FrontOp::Compose(ComposeKind::GroupReply)),
620            Forward => return Outcome::Front(FrontOp::Compose(ComposeKind::Forward)),
621            Resend => return Outcome::Front(FrontOp::Resend),
622            Edit => return Outcome::Front(FrontOp::RawEdit),
623            Attachments => return Outcome::Front(FrontOp::Attachments),
624            Folders => return Outcome::Front(FrontOp::Folders),
625            EnterCommand => return Outcome::Front(FrontOp::CommandPrompt),
626            Help => return Outcome::Front(FrontOp::Help),
627            Redraw => return Outcome::Front(FrontOp::Redraw),
628            PageTop => return Outcome::Front(FrontOp::PageMove(PageSpot::Top)),
629            PageMiddle => return Outcome::Front(FrontOp::PageMove(PageSpot::Middle)),
630            PageBottom => return Outcome::Front(FrontOp::PageMove(PageSpot::Bottom)),
631            SidebarToggle => return Outcome::Front(FrontOp::Sidebar(SidebarOp::Toggle)),
632            SidebarNext => return Outcome::Front(FrontOp::Sidebar(SidebarOp::Next)),
633            SidebarPrev => return Outcome::Front(FrontOp::Sidebar(SidebarOp::Prev)),
634            SidebarOpen => return Outcome::Front(FrontOp::Sidebar(SidebarOp::Open)),
635            TagPrefix => {
636                if !self.msgs.iter().any(|m| m.env.tagged) {
637                    self.note("no tagged messages");
638                    return Outcome::Done;
639                }
640                // mutt writes "Tag-" on its message line and waits;
641                // rmut's one bottom line appends it to the status bar,
642                // which then stays readable.
643                self.note("Tag-");
644                return Outcome::Front(FrontOp::TagPrefix);
645            }
646            Query => {
647                if self.config.mail.query_command.is_none() {
648                    self.error("no query_command configured");
649                    return Outcome::Done;
650                }
651                return Outcome::Front(FrontOp::Query);
652            }
653            Notmuch => {
654                if self.config.mail.notmuch == Some(false) {
655                    self.error("notmuch is disabled in the config");
656                    return Outcome::Done;
657                }
658                return Outcome::Front(FrontOp::Notmuch);
659            }
660            ChangeMailbox | ChangeMailboxReadOnly => {
661                if !self.ready_to_leave() {
662                    return Outcome::Done;
663                }
664                return Outcome::Front(FrontOp::ChangeMailbox {
665                    read_only: function == ChangeMailboxReadOnly,
666                });
667            }
668        }
669        Outcome::Done
670    }
671
672    /// One flag change, on the tagged set or on the message under the
673    /// cursor. mutt's $resolve (on by default) advances afterwards.
674    fn mark_selected(&mut self, tagged: bool, what: &'static str, f: impl Fn(&mut crate::Msg)) {
675        if self.deny_readonly() {
676            return;
677        }
678        if tagged {
679            self.each_tagged(what, f);
680        } else if let Some(&i) = self.visible.get(self.sel) {
681            self.push_undo(what, &[i]);
682            f(&mut self.msgs[i]);
683            self.msgs[i].dirty = true;
684            self.select(self.sel.saturating_add(1));
685        }
686    }
687}