103 lines
3.6 KiB
Rust
103 lines
3.6 KiB
Rust
//! Photoshop menu parity: how much of [`crate::menu_catalog::CATALOG`] is live.
|
||
//!
|
||
//! `cargo xtask parity` prints this report and writes `docs/parity.md`. A test keeps the live
|
||
//! count from regressing below [`FLOOR`].
|
||
|
||
use crate::menu_catalog::CATALOG;
|
||
use crate::menus::is_live;
|
||
|
||
/// Minimum number of live catalog items. Raise it when parity grows; never lower it.
|
||
pub const FLOOR: usize = 619;
|
||
|
||
/// One top-level menu's coverage.
|
||
#[derive(Clone, Debug, serde::Serialize)]
|
||
pub struct MenuParity {
|
||
pub menu: String,
|
||
pub live: usize,
|
||
pub total: usize,
|
||
/// Labels (with submenu path) of items not implemented yet.
|
||
pub missing: Vec<String>,
|
||
}
|
||
|
||
#[derive(Clone, Debug, serde::Serialize)]
|
||
pub struct Parity {
|
||
pub live: usize,
|
||
pub total: usize,
|
||
pub menus: Vec<MenuParity>,
|
||
}
|
||
|
||
impl Parity {
|
||
pub fn percent(&self) -> f64 {
|
||
if self.total == 0 { 0.0 } else { 100.0 * self.live as f64 / self.total as f64 }
|
||
}
|
||
|
||
/// The report as Markdown (the body of `docs/parity.md`).
|
||
pub fn to_markdown(&self) -> String {
|
||
let mut s = String::new();
|
||
s.push_str("# Photoshop menu parity\n\n");
|
||
s.push_str("Generated by `cargo xtask parity` from `crates/ui-egui/src/menu_catalog.rs` (Photoshop's menu tree) and the live command registry. Do not edit by hand.\n\n");
|
||
s.push_str(&format!("**{} of {} menu items live ({:.1}%).**\n\n", self.live, self.total, self.percent()));
|
||
s.push_str("| Menu | Live | Total | % |\n|---|---:|---:|---:|\n");
|
||
for m in &self.menus {
|
||
let pct = if m.total == 0 { 0.0 } else { 100.0 * m.live as f64 / m.total as f64 };
|
||
s.push_str(&format!("| {} | {} | {} | {:.0}% |\n", m.menu, m.live, m.total, pct));
|
||
}
|
||
s.push_str("\n## Not yet implemented\n");
|
||
for m in self.menus.iter().filter(|m| !m.missing.is_empty()) {
|
||
s.push_str(&format!("\n### {} ({})\n\n", m.menu, m.missing.len()));
|
||
for item in &m.missing {
|
||
s.push_str(&format!("- {item}\n"));
|
||
}
|
||
}
|
||
s
|
||
}
|
||
}
|
||
|
||
/// Compute coverage of the Photoshop menu catalog (separators excluded; duplicate ids counted once).
|
||
pub fn compute() -> Parity {
|
||
let mut menus: Vec<MenuParity> = Vec::new();
|
||
let mut seen = std::collections::HashSet::new();
|
||
for &(path, label, _, id) in CATALOG {
|
||
if id == "---" || !seen.insert(id) {
|
||
continue;
|
||
}
|
||
let top = path.first().copied().unwrap_or("?");
|
||
let m = match menus.iter().position(|m| m.menu == top) {
|
||
Some(i) => &mut menus[i],
|
||
None => {
|
||
menus.push(MenuParity { menu: top.to_string(), live: 0, total: 0, missing: Vec::new() });
|
||
menus.last_mut().expect("just pushed")
|
||
}
|
||
};
|
||
m.total += 1;
|
||
if is_live(id) {
|
||
m.live += 1;
|
||
} else {
|
||
let mut full: Vec<&str> = path[1..].to_vec();
|
||
full.push(label);
|
||
m.missing.push(format!("{} (`{id}`)", full.join(" › ")));
|
||
}
|
||
}
|
||
Parity { live: menus.iter().map(|m| m.live).sum(), total: menus.iter().map(|m| m.total).sum(), menus }
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn parity_does_not_regress() {
|
||
let p = compute();
|
||
assert!(p.total > 500, "catalog shrank: {}", p.total);
|
||
assert!(p.live >= FLOOR, "menu parity regressed: {} live < floor {FLOOR}", p.live);
|
||
}
|
||
|
||
#[test]
|
||
fn markdown_lists_every_missing_item() {
|
||
let p = compute();
|
||
let md = p.to_markdown();
|
||
let listed = md.lines().filter(|l| l.starts_with("- ")).count();
|
||
assert_eq!(listed, p.total - p.live);
|
||
}
|
||
}
|