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