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