1pub const DEFAULT_FORMAT: &str = "%4C %Z %-6d %-15.15L (%?l?%4l&%4c?) %s";
12
13pub const DEFAULT_STATUS_FORMAT: &str =
20 "---rmut%r: %f [Msgs:%?M?%M/?%m New:%n%?d? Del:%d?] (sort:%s)%?V? (limit:%V)?";
21
22pub struct IndexFields<'a> {
23 pub number: usize,
24 pub status: char,
25 pub flag: char,
26 pub mark: char,
29 pub date: &'a str,
30 pub from: &'a str,
31 pub size: &'a str,
32 pub lines: Option<usize>,
35 pub list: Option<&'a str>,
38 pub hidden: Option<usize>,
40 pub label: Option<&'a str>,
42 pub subject: &'a str,
43}
44
45fn value_of(spec: char, f: &IndexFields) -> String {
48 match spec {
49 'C' => f.number.to_string(),
50 'Z' => format!("{}{}{}", f.status, f.flag, f.mark),
51 'd' => f.date.to_string(),
52 'F' => f.from.to_string(),
53 'L' => match f.list {
54 Some(list) => format!("To {list}"),
55 None => f.from.to_string(),
56 },
57 'c' => f.size.to_string(),
58 'l' => f.lines.map(|n| n.to_string()).unwrap_or_default(),
59 'M' => f.hidden.map(|n| n.to_string()).unwrap_or_default(),
60 's' => f.subject.to_string(),
61 'y' => f.label.unwrap_or_default().to_string(),
62 '%' => "%".to_string(),
63 other => format!("%{other}"),
64 }
65}
66
67pub fn render(fmt: &str, f: &IndexFields) -> String {
68 render_with(fmt, &|spec| value_of(spec, f))
69}
70
71pub fn render_status(fmt: &str, width: usize, value_of: &dyn Fn(char) -> String) -> String {
75 let Some((left_fmt, rest)) = fmt.split_once("%>") else {
76 return render_with(fmt, value_of);
77 };
78 let mut rest = rest.chars();
79 let fill = rest.next().unwrap_or(' ');
80 let left = render_with(left_fmt, value_of);
81 let right = render_with(rest.as_str(), value_of);
82 let used = left.chars().count() + right.chars().count();
83 let gap = fill.to_string().repeat(width.saturating_sub(used));
84 format!("{left}{gap}{right}")
85}
86
87pub fn render_with(fmt: &str, value_of: &dyn Fn(char) -> String) -> String {
91 let mut out = String::new();
92 let mut chars = fmt.chars().peekable();
93 while let Some(c) = chars.next() {
94 if c != '%' {
95 out.push(c);
96 continue;
97 }
98 if chars.peek() == Some(&'?') {
100 chars.next();
101 let Some(spec) = chars.next() else { break };
102 if chars.peek() == Some(&'?') {
103 chars.next();
104 }
105 let mut then_part = String::new();
106 let mut else_part = String::new();
107 let mut cur = &mut then_part;
108 for c in chars.by_ref() {
109 match c {
110 '&' => cur = &mut else_part,
111 '?' => break,
112 c => cur.push(c),
113 }
114 }
115 let value = value_of(spec);
116 let chosen = if value.trim().is_empty() || value.trim() == "0" {
117 &else_part
118 } else {
119 &then_part
120 };
121 out.push_str(&render_with(chosen, value_of));
122 continue;
123 }
124 let mut left = false;
125 if chars.peek() == Some(&'-') {
126 left = true;
127 chars.next();
128 }
129 let mut min = 0usize;
130 while let Some(d) = chars.peek().and_then(|c| c.to_digit(10)) {
131 min = min * 10 + d as usize;
132 chars.next();
133 }
134 let mut max = usize::MAX;
135 if chars.peek() == Some(&'.') {
136 chars.next();
137 max = 0;
138 while let Some(d) = chars.peek().and_then(|c| c.to_digit(10)) {
139 max = max * 10 + d as usize;
140 chars.next();
141 }
142 }
143 let Some(spec) = chars.next() else { break };
144 let value = value_of(spec);
145 let mut v: Vec<char> = value.chars().collect();
146 if v.len() > max {
147 v.truncate(max);
148 }
149 let pad = min.saturating_sub(v.len());
150 if left {
151 out.extend(v);
152 out.extend(std::iter::repeat_n(' ', pad));
153 } else {
154 out.extend(std::iter::repeat_n(' ', pad));
155 out.extend(v);
156 }
157 }
158 out
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn status_right_align_fills_the_width() {
167 let v = |c: char| match c {
168 'a' => "A".to_string(),
169 'b' => "BB".to_string(),
170 _ => String::new(),
171 };
172 assert_eq!(render_status("%a%>-%b", 8, &v), "A-----BB");
173 assert_eq!(render_status("%a%> %b", 6, &v), "A BB");
175 assert_eq!(render_status("%a %b", 8, &v), "A BB");
177 assert_eq!(render_status("%a%>-%b", 2, &v), "ABB");
179 }
180
181 fn fields() -> IndexFields<'static> {
182 IndexFields {
183 number: 7,
184 status: 'N',
185 flag: '!',
186 mark: 'T',
187 date: "Jul 06",
188 from: "Jane Doe",
189 size: "1.2K",
190 lines: None,
191 list: None,
192 label: None,
193 hidden: None,
194 subject: "Lunch",
195 }
196 }
197
198 #[test]
199 fn default_format_matches_layout() {
200 assert_eq!(
201 render(DEFAULT_FORMAT, &fields()),
202 " 7 N!T Jul 06 Jane Doe (1.2K) Lunch"
203 );
204 }
205
206 #[test]
207 fn conditionals_and_list_alias() {
208 let f = fields();
209 assert_eq!(render("(%?l?%4l&%5c?)", &f), "( 1.2K)");
211 assert_eq!(render("%?s?have&none?", &f), "have");
212 assert_eq!(render("%?l?lines?", &f), "");
213 assert_eq!(render("%-10.10L|", &f), "Jane Doe |");
214 let counted = IndexFields {
216 lines: Some(42),
217 ..fields()
218 };
219 assert_eq!(render("(%?l?%4l&%5c?)", &counted), "( 42)");
220 assert_eq!(render("%l", &counted), "42");
221 let empty = IndexFields {
223 lines: Some(0),
224 ..fields()
225 };
226 assert_eq!(render("%?l?%l&-?", &empty), "-");
227 let listed = IndexFields {
229 list: Some("dev"),
230 ..fields()
231 };
232 assert_eq!(render("%-10.10L|", &listed), "To dev |");
233 assert_eq!(render("%F", &listed), "Jane Doe");
234 }
235
236 #[test]
237 fn width_precision_and_alignment() {
238 let f = fields();
239 assert_eq!(render("%-4.4F", &f), "Jane");
240 assert_eq!(render("%10s", &f), " Lunch");
241 assert_eq!(render("%.3s", &f), "Lun");
242 assert_eq!(render("100%% %C", &f), "100% 7");
243 }
244
245 #[test]
246 fn unknown_specifier_is_kept_visibly() {
247 assert_eq!(render("%q", &fields()), "%q");
248 }
249}