1use std::sync::Arc;
10
11use crate::fragment::{DashboardFragment, DashboardRole, DashboardRow, DashboardSpan, LinkTarget};
12use crate::registry::DashboardRegistry;
13use crate::section::{DashboardCtx, DashboardSection};
14
15const REPO_URL: &str = "https://github.com/dhruvasagar/lattice";
17pub fn builtin_registry() -> DashboardRegistry {
25 let mut reg = DashboardRegistry::new();
26 reg.register(Arc::new(About));
27 reg.register(Arc::new(Survival));
28 reg.register(Arc::new(Links));
29 reg.register(Arc::new(Tutor));
30 reg.register(Arc::new(Commands));
31 reg.register(Arc::new(HelpAndBindings));
32 reg.register(Arc::new(Describe));
33 reg.register(Arc::new(HelpTopics));
34 reg
35}
36
37fn heading(frag: &mut DashboardFragment, text: &str) {
39 frag.push(DashboardRow::line(text, DashboardRole::SectionHeading));
40}
41
42fn body_link(prefix: &str, label: &str, target: LinkTarget) -> DashboardRow {
46 DashboardRow::line(prefix, DashboardRole::Body).push(DashboardSpan::link(label, target))
47}
48
49struct About;
55
56impl DashboardSection for About {
57 fn id(&self) -> &str {
58 "about"
59 }
60 fn order(&self) -> i32 {
61 10
62 }
63 fn render(&self, _ctx: &DashboardCtx) -> DashboardFragment {
64 let mut f = DashboardFragment::new();
65 heading(&mut f, "About");
66 f.line(
67 "Lattice combines vim's modal editing with emacs's extensibility",
68 DashboardRole::Body,
69 );
70 f.line(
71 "on a non-blocking, multi-threaded, GPU-accelerated core.",
72 DashboardRole::Body,
73 );
74 f.blank();
75 f.line("Paramount goals:", DashboardRole::Body);
76 f.line(
77 " 1. Performance — imperceptible keystroke latency",
78 DashboardRole::Body,
79 );
80 f.line(
81 " 2. Extensibility — WebAssembly plugins from day one",
82 DashboardRole::Body,
83 );
84 f.line(" 3. Extensible vim modal editing", DashboardRole::Body);
85 f.line(
86 " 4. Asynchronicity — nothing blocks the UI",
87 DashboardRole::Body,
88 );
89 f
90 }
91}
92
93struct Survival;
101
102impl DashboardSection for Survival {
103 fn id(&self) -> &str {
104 "survival"
105 }
106 fn order(&self) -> i32 {
107 15
108 }
109 fn render(&self, _ctx: &DashboardCtx) -> DashboardFragment {
110 let mut f = DashboardFragment::new();
111 heading(&mut f, "Survival");
112 f.push(body_link(
113 "Quit ",
114 ":q",
115 LinkTarget::Command("q".to_string()),
116 ));
117 f.push(
118 DashboardRow::line("Config ", DashboardRole::Body)
119 .push(DashboardSpan::new(
120 "lattice --scaffold-init",
121 DashboardRole::Key,
122 ))
123 .push(DashboardSpan::new(
124 " — writes a starter WASM config crate (Cargo.toml, plugin.toml, src/lib.rs)",
125 DashboardRole::Hint,
126 )),
127 );
128 f
129 }
130}
131
132struct Links;
138
139impl DashboardSection for Links {
140 fn id(&self) -> &str {
141 "links"
142 }
143 fn order(&self) -> i32 {
144 20
145 }
146 fn render(&self, _ctx: &DashboardCtx) -> DashboardFragment {
147 let mut f = DashboardFragment::new();
148 heading(&mut f, "Links");
149 f.push(body_link(
150 "GitHub ",
151 REPO_URL,
152 LinkTarget::Url(REPO_URL.to_string()),
153 ));
154 f.push(body_link(
155 "Issues ",
156 "report a bug",
157 LinkTarget::Url(format!("{REPO_URL}/issues")),
158 ));
159 f
160 }
161}
162
163struct Tutor;
169
170impl DashboardSection for Tutor {
171 fn id(&self) -> &str {
172 "tutor"
173 }
174 fn order(&self) -> i32 {
175 40
176 }
177 fn render(&self, _ctx: &DashboardCtx) -> DashboardFragment {
178 let mut f = DashboardFragment::new();
179 heading(&mut f, "Learn");
180 f.push(body_link(
181 "Interactive lessons ",
182 ":tutor",
183 LinkTarget::Command("tutor".to_string()),
184 ));
185 f
186 }
187}
188
189struct HelpAndBindings;
195
196impl DashboardSection for HelpAndBindings {
197 fn id(&self) -> &str {
198 "help-and-bindings"
199 }
200 fn order(&self) -> i32 {
201 50
202 }
203 fn render(&self, _ctx: &DashboardCtx) -> DashboardFragment {
204 let mut f = DashboardFragment::new();
205 heading(&mut f, "Help & key bindings");
206 f.push(body_link(
207 "Help browser ",
208 ":help",
209 LinkTarget::Command("help".to_string()),
210 ));
211 f.push(body_link(
212 "What does a key do? ",
213 ":describe-key",
214 LinkTarget::Command("describe-key".to_string()),
215 ));
216 f.push(body_link(
217 "All key bindings ",
218 ":keymap",
219 LinkTarget::Command("keymap".to_string()),
220 ));
221 f
222 }
223}
224
225struct Describe;
231
232impl DashboardSection for Describe {
233 fn id(&self) -> &str {
234 "describe"
235 }
236 fn order(&self) -> i32 {
237 60
238 }
239 fn render(&self, _ctx: &DashboardCtx) -> DashboardFragment {
240 let mut f = DashboardFragment::new();
241 heading(&mut f, "Introspection (:describe-*)");
242 for (cmd, what) in [
250 ("describe-key", "what a key is bound to"),
251 ("describe-command", "what a command does"),
252 ("describe-option", "an option's type and value"),
253 ("describe-mode", "the active modes"),
254 ("apropos", "search everything by keyword"),
255 ] {
256 f.push(
257 body_link(
258 "",
259 &format!(":{cmd}"),
260 LinkTarget::Command(format!("describe-command {cmd}")),
261 )
262 .push(DashboardSpan::new(
263 format!(" — {what}"),
264 DashboardRole::Hint,
265 )),
266 );
267 }
268 f
269 }
270}
271
272struct Commands;
280
281impl DashboardSection for Commands {
282 fn id(&self) -> &str {
283 "commands"
284 }
285 fn order(&self) -> i32 {
286 45
287 }
288 fn render(&self, _ctx: &DashboardCtx) -> DashboardFragment {
289 let mut f = DashboardFragment::new();
290 heading(&mut f, "Commonly useful commands");
291 for (cmd, what) in [
292 ("files", "fuzzy-find files in the project"),
293 ("buffers", "switch buffers (fuzzy picker)"),
294 ("recent", "reopen a recently edited file"),
295 ("marks", "jump to a mark"),
296 ("registers", "inspect register contents"),
297 ] {
298 f.push(
299 body_link("", &format!(":{cmd}"), LinkTarget::Command(cmd.to_string())).push(
300 DashboardSpan::new(format!(" — {what}"), DashboardRole::Hint),
301 ),
302 );
303 }
304 f
305 }
306}
307
308struct HelpTopics;
314
315impl DashboardSection for HelpTopics {
316 fn id(&self) -> &str {
317 "help-topics"
318 }
319 fn order(&self) -> i32 {
320 70
321 }
322 fn render(&self, _ctx: &DashboardCtx) -> DashboardFragment {
323 let mut f = DashboardFragment::new();
324 heading(&mut f, "Help topics");
325 for topic in ["getting-started", "modes", "ex-commands", "options"] {
330 f.push(body_link(
331 "",
332 &format!(":help {topic}"),
333 LinkTarget::Topic(topic.to_string()),
334 ));
335 }
336 f
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use crate::registry::SectionSelection;
344
345 #[test]
346 fn builtin_registry_sections_in_order() {
347 let reg = builtin_registry();
349 let ids: Vec<String> = reg
350 .ordered(&SectionSelection::Default)
351 .iter()
352 .map(|s| s.id().to_string())
353 .collect();
354 assert_eq!(
355 ids,
356 [
357 "about",
358 "survival",
359 "links",
360 "tutor",
361 "commands",
362 "help-and-bindings",
363 "describe",
364 "help-topics",
365 ]
366 );
367 }
368
369 #[test]
370 fn every_builtin_renders_non_empty() {
371 let reg = builtin_registry();
372 let ctx = DashboardCtx::default();
373 for section in reg.ordered(&SectionSelection::Default) {
374 let frag = section.render(&ctx);
375 assert!(
376 !frag.is_empty(),
377 "section {} rendered an empty fragment",
378 section.id()
379 );
380 }
381 }
382
383 #[test]
384 fn links_section_carries_followable_links() {
385 let reg = builtin_registry();
386 let ctx = DashboardCtx::default();
387 let links = reg
388 .ordered(&SectionSelection::Explicit(vec!["links".into()]))
389 .into_iter()
390 .next()
391 .unwrap();
392 let frag = links.render(&ctx);
393 let has_link = frag
394 .rows
395 .iter()
396 .flat_map(|r| &r.spans)
397 .any(|s| matches!(&s.link, Some(LinkTarget::Url(u)) if u.contains("github.com")));
398 assert!(has_link, "links section should carry a GitHub url link");
399 }
400
401 #[test]
402 fn tutor_section_links_to_tutor_command() {
403 let reg = builtin_registry();
404 let frag = reg
405 .ordered(&SectionSelection::Explicit(vec!["tutor".into()]))
406 .into_iter()
407 .next()
408 .unwrap()
409 .render(&DashboardCtx::default());
410 let has_cmd = frag
411 .rows
412 .iter()
413 .flat_map(|r| &r.spans)
414 .any(|s| matches!(&s.link, Some(LinkTarget::Command(c)) if c == "tutor"));
415 assert!(has_cmd, "tutor section should link to cmd:tutor");
416 }
417
418 fn command_links(section_id: &str) -> Vec<String> {
420 let reg = builtin_registry();
421 reg.ordered(&SectionSelection::Explicit(vec![section_id.into()]))
422 .into_iter()
423 .next()
424 .unwrap_or_else(|| panic!("section {section_id} missing"))
425 .render(&DashboardCtx::default())
426 .rows
427 .iter()
428 .flat_map(|r| &r.spans)
429 .filter_map(|s| match &s.link {
430 Some(LinkTarget::Command(c)) => Some(c.clone()),
431 _ => None,
432 })
433 .collect()
434 }
435
436 #[test]
437 fn describe_section_links_to_help_pages_not_bare_commands() {
438 let cmds = command_links("describe");
442 assert!(!cmds.is_empty(), "describe section should carry links");
443 assert!(
444 cmds.iter().all(|c| c.starts_with("describe-command ")),
445 "all introspection links must open help pages: {cmds:?}"
446 );
447 assert!(
448 cmds.iter().any(|c| c == "describe-command describe-key"),
449 "expected a help-page link for describe-key: {cmds:?}"
450 );
451 }
452
453 #[test]
454 fn the_dashboard_tells_a_first_time_user_how_to_leave_and_how_to_configure() {
455 let survival = Survival.render(&DashboardCtx::default());
461 let survival_text = survival
462 .rows
463 .iter()
464 .map(|row| row.text())
465 .collect::<Vec<_>>()
466 .join("\n");
467 assert!(
468 survival_text.contains(":q"),
469 "the Survival row must say how to quit"
470 );
471 assert!(
472 survival_text.contains("lattice --scaffold-init"),
473 "the Survival row must point at config scaffolding"
474 );
475 assert!(
479 survival_text.contains("src/lib.rs"),
480 "the Survival row must describe what --scaffold-init actually writes"
481 );
482 assert!(
483 !survival_text.contains("config.toml"),
484 "the Survival row must not claim --scaffold-init writes config.toml -- it doesn't"
485 );
486 }
487
488 #[test]
489 fn commands_section_runs_useful_commands() {
490 let cmds = command_links("commands");
493 for expect in ["files", "buffers", "recent", "marks", "registers"] {
494 assert!(
495 cmds.iter().any(|c| c == expect),
496 "missing :{expect} run-link in {cmds:?}"
497 );
498 }
499 }
500}