Skip to main content

rmut_front/
status.rs

1//! The status bar and the terminal title: mutt's $status_format,
2//! $ts_status_format and $pager_format, expanded from the session.
3//! Both front ends show the same line.
4
5use rmut_core::config::Config;
6use rmut_core::format;
7use rmut_core::message::MessageView;
8use rmut_session::Session;
9
10use crate::pager::{PagerStyle, pager_line_count};
11
12/// mutt's $wrap: the pager's text width at a screen width. Positive
13/// caps it, negative leaves that margin (never under 20 columns).
14pub fn pager_wrap(config: &Config, width: usize) -> usize {
15    match config.pager.wrap {
16        Some(n) if n > 0 => (n as usize).min(width),
17        Some(n) if n < 0 => width.saturating_sub(n.unsigned_abs() as usize).max(20),
18        _ => width,
19    }
20}
21
22/// What the pager's status line needs to know about the open message.
23pub struct PagerView<'a> {
24    pub view: &'a MessageView,
25    pub scroll: usize,
26    pub full_headers: bool,
27    pub hide_quoted: bool,
28}
29
30pub fn index_status(session: &Session, index_offset: usize, width: usize, rows: usize) -> String {
31    let fmt = session
32        .config
33        .ui
34        .status_format
35        .as_deref()
36        .unwrap_or(format::DEFAULT_STATUS_FORMAT);
37    format::render_status(fmt, width, &|spec| {
38        index_status_field(session, index_offset, spec, rows)
39    })
40}
41
42/// mutt's $ts_status_format: the terminal title, from the same fields
43/// as the status line (a wide width, so %>… padding does not clip).
44pub fn index_title(session: &Session, index_offset: usize, rows: usize) -> String {
45    let fmt = session
46        .config
47        .ui
48        .title_format
49        .as_deref()
50        .unwrap_or("rmut: %f");
51    format::render_status(fmt, 200, &|spec| {
52        index_status_field(session, index_offset, spec, rows)
53    })
54    .trim_end()
55    .to_string()
56}
57
58/// One status specifier's value, shared by the bottom bar and the
59/// terminal title.
60fn index_status_field(session: &Session, index_offset: usize, spec: char, rows: usize) -> String {
61    match spec {
62        'f' => session.title.clone(),
63        'm' => session.msgs.len().to_string(),
64        // Shown message count, only when a limit narrows the view.
65        'M' => {
66            if session.visible.len() != session.msgs.len() {
67                session.visible.len().to_string()
68            } else {
69                String::new()
70            }
71        }
72        'n' => session.new_count().to_string(),
73        'u' => session
74            .msgs
75            .iter()
76            .filter(|m| !m.env.file.flags.seen)
77            .count()
78            .to_string(),
79        'd' => session.deleted_count().to_string(),
80        'F' => session
81            .msgs
82            .iter()
83            .filter(|m| m.env.file.flags.flagged)
84            .count()
85            .to_string(),
86        't' => session
87            .msgs
88            .iter()
89            .filter(|m| m.env.tagged)
90            .count()
91            .to_string(),
92        's' => format!(
93            "{}{}",
94            session.sort.name(),
95            if session.sort_rev { "-rev" } else { "" }
96        ),
97        'V' => session
98            .limit
99            .as_ref()
100            .map(|(s, _)| s.clone())
101            .unwrap_or_default(),
102        'r' => {
103            // mutt's $status_chars: [0] unchanged, [1] changed, [2]
104            // read-only. Unset keeps rmut's own marks.
105            let chars: Option<Vec<char>> = session
106                .config
107                .ui
108                .status_chars
109                .as_deref()
110                .map(|s| s.chars().collect());
111            let pick = |i: usize, default: &str| -> String {
112                chars
113                    .as_ref()
114                    .and_then(|c| c.get(i))
115                    .map(|c| c.to_string())
116                    .unwrap_or_else(|| default.to_string())
117            };
118            if session.read_only {
119                pick(2, "%")
120            } else if session.pending_count() > 0 {
121                pick(1, "*")
122            } else {
123                pick(0, "")
124            }
125        }
126        'v' => env!("CARGO_PKG_VERSION").to_string(),
127        // Index scroll position, like mutt's %P.
128        'P' => {
129            let len = session.visible.len();
130            if len <= rows {
131                "all".into()
132            } else if index_offset == 0 {
133                "top".into()
134            } else if index_offset + rows >= len {
135                "bot".into()
136            } else {
137                format!("{}%", (index_offset + rows) * 100 / len)
138            }
139        }
140        '%' => "%".to_string(),
141        other => format!("%{other}"),
142    }
143}
144
145/// The classic pager bottom line; override with `[pager] format`.
146pub const DEFAULT_PAGER_FORMAT: &str = "---Message %C/%m: %s -- %P";
147
148/// mutt's $pager_format: %C message number, %m count, %n sender,
149/// %s subject, %Z status chars, %P percent through the message,
150/// %f mailbox, plus the conditional and %> machinery.
151pub fn pager_status(
152    session: &Session,
153    pager: &PagerView,
154    content_height: usize,
155    width: usize,
156) -> String {
157    let total = pager_line_count(
158        pager.view,
159        pager_wrap(&session.config, width),
160        pager.full_headers,
161        &PagerStyle::of(&session.config, &session.quote_re),
162        pager.hide_quoted,
163    )
164    .max(1);
165    let shown = (pager.scroll + content_height).min(total);
166    let subject = pager
167        .view
168        .brief
169        .iter()
170        .find(|(n, _)| n == "Subject" || n == "Content-Type")
171        .map(|(_, v)| v.as_str())
172        .unwrap_or("");
173    let msg = session.visible.get(session.sel).map(|&i| &session.msgs[i]);
174    let fmt = session
175        .config
176        .pager
177        .format
178        .as_deref()
179        .unwrap_or(DEFAULT_PAGER_FORMAT);
180    format::render_status(fmt, width, &|spec| match spec {
181        'C' => (session.sel + 1).to_string(),
182        'm' => session.visible.len().to_string(),
183        's' => subject.to_string(),
184        'n' => msg.map(|m| m.env.from.clone()).unwrap_or_default(),
185        'Z' => msg
186            .map(|m| {
187                let f = &m.env.file;
188                format!(
189                    "{}{} ",
190                    f.flags.status_char(f.is_new),
191                    if f.flags.flagged { '!' } else { ' ' }
192                )
193            })
194            .unwrap_or_default(),
195        'P' => format!("{}%", shown * 100 / total),
196        'f' => session.title.clone(),
197        '%' => "%".to_string(),
198        other => format!("%{other}"),
199    })
200}