Files
solidcraft-upstream/xtask/src/features.rs
T
Irakli KourtisandClaude Opus 5.5 c8d668108a Surface toolset: icons and planned commands (#808)
Icons for the surface toolset, and its planned commands registered and disabled with the issue that tracks each one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:28:38 -04:00

120 lines
5.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `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<String> = 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 <id> '<json>'`, 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 |"));
}
}