//! `cargo xtask features`: SolidCraft's own feature checklist, `docs/features.md`. //! //! The page lists every registered command by area (the first part of its dotted id), with its //! label, id and toolbar place. It comes only from SolidCraft's command registry //! (`solidcraft-cli commands`), so it can't drift from what the program does; it compares with //! nothing else. Planned commands (registered, not built yet) are listed as planned, with their //! issue, and are not counted. use std::path::Path; use serde_json::Value; /// Areas in the order the page lists them (id prefix, heading). Other prefixes follow, /// alphabetically, under their prefix. const AREAS: &[(&str, &str)] = &[ ("sketch", "Sketch"), ("solid", "Solid modelling"), ("surface", "Surfaces"), ("construct", "Construction geometry"), ("sheet", "Sheet metal"), ("plastic", "Plastic parts"), ("component", "Components"), ("joint", "Assembly joints"), ("config", "Configurations"), ("timeline", "Timeline"), ("inspect", "Inspect"), ("file", "Files"), ("doc", "Documents"), ("edit", "Edit"), ]; pub fn run(root: &Path) -> Result<(), String> { let out = crate::cargo() .args(["run", "-q", "--release", "-p", "solidcraft-cli", "--", "commands"]) .output() .map_err(|e| format!("solidcraft-cli: {e}"))?; if !out.status.success() { return Err(format!("solidcraft-cli commands failed:\n{}", String::from_utf8_lossy(&out.stderr))); } let cmds: Value = serde_json::from_slice(&out.stdout).map_err(|e| format!("commands JSON: {e}"))?; let list = cmds.as_array().map(Vec::as_slice).unwrap_or_default(); let md = page(list); std::fs::write(root.join("docs/features.md"), md).map_err(|e| format!("docs/features.md: {e}"))?; println!("features: {} commands → docs/features.md", list.len()); Ok(()) } fn area(id: &str) -> &str { id.split('.').next().unwrap_or_default() } /// The checklist page for the registry's commands. pub fn page(cmds: &[Value]) -> String { let s = |c: &Value, k: &str| c[k].as_str().unwrap_or_default().to_string(); let mut prefixes: Vec = cmds.iter().map(|c| area(&s(c, "id")).to_string()).collect(); prefixes.sort(); prefixes.dedup(); let mut order: Vec<(String, String)> = AREAS.iter().filter(|(p, _)| prefixes.iter().any(|x| x == p)).map(|(p, h)| (p.to_string(), h.to_string())).collect(); for p in prefixes.iter().filter(|p| !AREAS.iter().any(|(a, _)| a == p)) { order.push((p.clone(), format!("`{p}.*`"))); } let planned = |c: &Value| c["planned"].as_str().map(str::to_string); let built = cmds.iter().filter(|c| planned(c).is_none()).count(); let later = match cmds.len() - built { 0 => String::new(), n => format!(" Planned (registered, not built yet): {n}."), }; let mut md = format!( "# Features\n\nSolidCraft's commands by area, generated by `cargo xtask features` from the command registry \ (`solidcraft-cli commands`). **{built} commands.**{later} Every command runs the same way from the toolbar, a script, \ `solidcraft-cli exec ''`, the control channel and MCP; parameters are in the \ [command reference](book/src/reference/commands.md) (`cargo xtask book`). Status and plans: [ROADMAP](../ROADMAP.md).\n" ); for (prefix, heading) in &order { let in_area: Vec<&Value> = cmds.iter().filter(|c| area(&s(c, "id")) == prefix).collect(); let n = in_area.iter().filter(|c| planned(c).is_none()).count(); let count = match in_area.len() - n { 0 => n.to_string(), p => format!("{n}, {p} planned"), }; md += &format!("\n## {heading} ({count})\n\n| Command | Id | Toolbar |\n|---|---|---|\n"); for c in in_area { let place = match (s(c, "tab"), s(c, "panel")) { (t, _) if t.is_empty() => "–".to_string(), (t, p) if p.is_empty() => t, (t, p) => format!("{t} › {p}"), }; let key = c["shortcut"].as_str().map(|k| format!(" ({k})")).unwrap_or_default(); let note = planned(c).map(|i| format!(" *(planned: [#{i}](https://github.com/bherbruck/solidcraft/issues/{i}))*")).unwrap_or_default(); md += &format!("| {}{key}{note} | `{}` | {place} |\n", s(c, "label").replace('|', "/"), s(c, "id")); } } md } #[cfg(test)] mod tests { use super::*; #[test] fn page_groups_by_area() { let cmds: Value = serde_json::from_str( r#"[{"id":"solid.extrude","label":"Extrude","tab":"SOLID","panel":"CREATE","shortcut":"E"}, {"id":"sketch.line","label":"Line","tab":"SKETCH","panel":"CREATE"}, {"id":"view.fit","label":"Fit","tab":"","panel":""}, {"id":"solid.knit","label":"Knit","tab":"SOLID","panel":"CREATE","planned":"9"}]"#, ) .unwrap(); let md = page(cmds.as_array().unwrap()); assert!(md.contains("**3 commands.** Planned (registered, not built yet): 1.")); let (sk, so, other) = (md.find("## Sketch (1)").unwrap(), md.find("## Solid modelling (1, 1 planned)").unwrap(), md.find("## `view.*` (1)").unwrap()); assert!(sk < so && so < other); assert!(md.contains("| Extrude (E) | `solid.extrude` | SOLID › CREATE |")); assert!(md.contains("| Fit | `view.fit` | – |")); assert!(md.contains("| Knit *(planned: [#9](https://github.com/bherbruck/solidcraft/issues/9))* | `solid.knit` | SOLID › CREATE |")); } }