1use crate::format::DisplayTemplate;
12
13const PROMPT_FIELDS: &[&str] = &[
17 "label",
18 "top",
19 "bottom",
20 "total",
21 "pct",
22 "rec-top",
23 "rec-bottom",
24 "rec-total",
25 "rec-block",
26 "wrap-offset",
27 "col-offset",
28 "format-tag",
29 "filter-tag",
30 "grep-tag",
31 "or-tag",
32 "hide-tag",
33 "search-tag",
34 "pretty-tag",
35 "live-tag",
36 "follow-tag",
37 "preprocess-failed-tag",
38 "file-index-tag",
39 "tag-tag",
40];
41
42#[derive(Debug, Clone)]
43pub struct ParsedPrompt {
44 template: DisplayTemplate,
45}
46
47impl ParsedPrompt {
48 pub fn parse(source: &str) -> Result<Self, String> {
51 let field_names: Vec<String> =
52 PROMPT_FIELDS.iter().map(|s| s.to_string()).collect();
53 let template = DisplayTemplate::compile(source, &field_names)?;
54 Ok(Self { template })
55 }
56
57 pub fn render(&self, ctx: &PromptContext) -> String {
59 self.template.render(|name| ctx.lookup(name))
60 }
61
62 pub fn source(&self) -> &str {
63 self.template.source()
64 }
65}
66
67#[derive(Debug, Default)]
70pub struct PromptContext {
71 pub label: String,
72 pub top: usize,
73 pub bottom: usize,
74 pub total: usize,
75 pub pct: u8,
76 pub rec_top: usize,
77 pub rec_bottom: usize,
78 pub rec_total: usize,
79 pub records_mode: bool,
80 pub wrap_offset: String,
81 pub col_offset: String,
82 pub format_tag: String,
83 pub filter_tag: String,
84 pub grep_tag: String,
85 pub or_tag: String,
86 pub hide_tag: String,
87 pub search_tag: String,
88 pub pretty_tag: String,
89 pub live_tag: String,
90 pub follow_tag: String,
91 pub preprocess_failed_tag: String,
92 pub file_index_tag: String,
93 pub tag_tag: String,
94}
95
96impl PromptContext {
97 fn lookup(&self, name: &str) -> Option<String> {
98 match name {
99 "label" => Some(self.label.clone()),
100 "top" => Some(self.top.to_string()),
101 "bottom" => Some(self.bottom.to_string()),
102 "total" => Some(self.total.to_string()),
103 "pct" => Some(self.pct.to_string()),
104 "rec-top" => Some(self.rec_top.to_string()),
105 "rec-bottom" => Some(self.rec_bottom.to_string()),
106 "rec-total" => Some(self.rec_total.to_string()),
107 "rec-block" => Some(if self.records_mode {
108 format!(
109 "L{}-{}/{} R{}-{}/{}",
110 self.top, self.bottom, self.total,
111 self.rec_top, self.rec_bottom, self.rec_total,
112 )
113 } else {
114 format!("{}-{}/{}", self.top, self.bottom, self.total)
115 }),
116 "wrap-offset" => Some(self.wrap_offset.clone()),
117 "col-offset" => Some(self.col_offset.clone()),
118 "format-tag" => Some(self.format_tag.clone()),
119 "filter-tag" => Some(self.filter_tag.clone()),
120 "grep-tag" => Some(self.grep_tag.clone()),
121 "or-tag" => Some(self.or_tag.clone()),
122 "hide-tag" => Some(self.hide_tag.clone()),
123 "search-tag" => Some(self.search_tag.clone()),
124 "pretty-tag" => Some(self.pretty_tag.clone()),
125 "live-tag" => Some(self.live_tag.clone()),
126 "follow-tag" => Some(self.follow_tag.clone()),
127 "preprocess-failed-tag" => Some(self.preprocess_failed_tag.clone()),
128 "file-index-tag" => Some(self.file_index_tag.clone()),
129 "tag-tag" => Some(self.tag_tag.clone()),
130 _ => None,
131 }
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn parse_literal_only_template() {
141 let p = ParsedPrompt::parse("hello").unwrap();
142 let ctx = PromptContext::default();
143 assert_eq!(p.render(&ctx), "hello");
144 }
145
146 #[test]
147 fn parse_field_template() {
148 let p = ParsedPrompt::parse("<label> <pct>%").unwrap();
149 let ctx = PromptContext {
150 label: "file.log".into(),
151 pct: 42,
152 ..Default::default()
153 };
154 assert_eq!(p.render(&ctx), "file.log 42%");
155 }
156
157 #[test]
158 fn parse_rejects_unknown_field() {
159 let err = ParsedPrompt::parse("<bogus>").unwrap_err();
160 assert!(err.contains("bogus"), "error mentions field name: {err}");
161 }
162
163 #[test]
164 fn parse_handles_escaped_left_angle() {
165 let p = ParsedPrompt::parse(r"\<not a field>").unwrap();
167 let ctx = PromptContext::default();
168 assert_eq!(p.render(&ctx), "<not a field>");
169 }
170
171 #[test]
172 fn parse_handles_escaped_backslash() {
173 let p = ParsedPrompt::parse(r"a\\b").unwrap();
174 let ctx = PromptContext::default();
175 assert_eq!(p.render(&ctx), "a\\b");
176 }
177
178 #[test]
179 fn render_resolves_empty_tags_to_nothing() {
180 let p = ParsedPrompt::parse("<label><filter-tag><grep-tag>").unwrap();
181 let ctx = PromptContext { label: "x".into(), ..Default::default() };
182 assert_eq!(p.render(&ctx), "x");
183 }
184
185 #[test]
186 fn or_tag_renders() {
187 let p = ParsedPrompt::parse("<grep-tag><or-tag>").unwrap();
188 let ctx = PromptContext { grep_tag: " [grep]".into(), or_tag: " [or]".into(), ..Default::default() };
189 assert_eq!(p.render(&ctx), " [grep] [or]");
190 }
191
192 #[test]
193 fn render_resolves_populated_tags() {
194 let p = ParsedPrompt::parse("<grep-tag><hide-tag>").unwrap();
195 let ctx = PromptContext {
196 grep_tag: " [grep]".into(),
197 hide_tag: " [hide]".into(),
198 ..Default::default()
199 };
200 assert_eq!(p.render(&ctx), " [grep] [hide]");
201 }
202
203 #[test]
204 fn rec_block_renders_records_mode_form() {
205 let p = ParsedPrompt::parse("<rec-block>").unwrap();
206 let ctx = PromptContext {
207 top: 1, bottom: 3, total: 3,
208 rec_top: 1, rec_bottom: 2, rec_total: 2,
209 records_mode: true,
210 ..Default::default()
211 };
212 assert_eq!(p.render(&ctx), "L1-3/3 R1-2/2");
213 }
214
215 #[test]
216 fn rec_block_renders_line_mode_form() {
217 let p = ParsedPrompt::parse("<rec-block>").unwrap();
218 let ctx = PromptContext {
219 top: 1, bottom: 3, total: 3,
220 records_mode: false,
221 ..Default::default()
222 };
223 assert_eq!(p.render(&ctx), "1-3/3");
224 }
225
226 #[test]
227 fn render_preprocess_failed_tag_resolves_when_populated() {
228 let p = ParsedPrompt::parse("<preprocess-failed-tag>").unwrap();
229 let ctx = PromptContext {
230 preprocess_failed_tag: " [preprocess-failed: bad cmd]".into(),
231 ..Default::default()
232 };
233 assert_eq!(p.render(&ctx), " [preprocess-failed: bad cmd]");
234 }
235
236 #[test]
237 fn render_file_index_tag_resolves_when_populated() {
238 let p = ParsedPrompt::parse("<file-index-tag>").unwrap();
239 let ctx = PromptContext {
240 file_index_tag: " [2/3]".into(),
241 ..Default::default()
242 };
243 assert_eq!(p.render(&ctx), " [2/3]");
244 }
245
246 #[test]
247 fn render_file_index_tag_empty_when_unset() {
248 let p = ParsedPrompt::parse("<file-index-tag>").unwrap();
249 let ctx = PromptContext::default();
250 assert_eq!(p.render(&ctx), "");
251 }
252
253 #[test]
254 fn render_tag_tag_resolves_when_populated() {
255 let p = ParsedPrompt::parse("<tag-tag>").unwrap();
256 let ctx = PromptContext {
257 tag_tag: " [tag: foo (2/3)]".into(),
258 ..Default::default()
259 };
260 assert_eq!(p.render(&ctx), " [tag: foo (2/3)]");
261 }
262
263 #[test]
264 fn render_tag_tag_empty_when_unset() {
265 let p = ParsedPrompt::parse("<tag-tag>").unwrap();
266 let ctx = PromptContext::default();
267 assert_eq!(p.render(&ctx), "");
268 }
269
270 #[test]
271 fn col_offset_renders() {
272 let p = ParsedPrompt::parse("<wrap-offset><col-offset>").unwrap();
273 let ctx = PromptContext { col_offset: " \u{00bb}12".into(), ..Default::default() };
274 assert_eq!(p.render(&ctx), " \u{00bb}12");
275 }
276}