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