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