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