use sui_spec::catalog::{self, MaturityGate, SubstrateDomain};
use sui_spec::cli_coverage::{self, SuiCommand, SuiCommandMaturity};
use sui_spec::hash;
use sui_spec::lock_file::{self, ParsedLockFile};
use sui_spec::narinfo::{self, ParsedNarInfo};
use sui_spec::operator_view::{render as render_view, OperatorView};
use sui_spec::realisation::{self, ParsedRealisation};
use sui_spec::registry::{self, RegistryEntry, RegistryScope};
use sui_spec::store_layout::{self, ParsedStorePath};
use sui_spec::style::{
self, body, dim_fg, error, glyph_arrow, glyph_gear, glyph_ok, glyph_snowflake,
header, ident, info, muted, pending, success, warn, LabeledTable, NORD13, NORD15, NORD3, NORD8,
};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse()?;
if let Some(path) = &args.flake_lock {
emit_flake_lock(path)?;
return Ok(());
}
if let Some(path) = &args.narinfo_path {
emit_narinfo(path)?;
return Ok(());
}
if let Some(flake_ref) = &args.registry_resolve {
emit_registry_resolve(flake_ref)?;
return Ok(());
}
if let Some(path) = &args.realisation_path {
emit_realisation(path)?;
return Ok(());
}
if let Some(input) = &args.hash_decode {
emit_hash_decode(input)?;
return Ok(());
}
if let Some(path) = &args.store_path {
emit_store_path(path)?;
return Ok(());
}
if args.coverage {
emit_coverage(&args)?;
return Ok(());
}
let cat = if args.topo {
catalog::topological_order()?
} else {
catalog::load_canonical()?
};
if args.histogram {
emit_histogram(&cat);
return Ok(());
}
let filtered: Vec<&SubstrateDomain> = cat
.iter()
.filter(|d| match &args.domain_filter {
Some(name) => d.name == *name,
None => true,
})
.filter(|d| match &args.gate_filter {
Some(gate) => &gate_name(d.gate) == gate,
None => true,
})
.collect();
if filtered.is_empty() {
eprintln!("no domains match the filter");
std::process::exit(1);
}
if args.json {
let body = serde_json::to_string_pretty(&filtered)?;
println!("{body}");
} else {
emit_table(&filtered);
}
Ok(())
}
struct Args {
json: bool,
histogram: bool,
topo: bool,
gate_filter: Option<String>,
domain_filter: Option<String>,
flake_lock: Option<String>,
narinfo_path: Option<String>,
registry_resolve: Option<String>,
realisation_path: Option<String>,
hash_decode: Option<String>,
store_path: Option<String>,
coverage: bool,
coverage_filter: Option<SuiCommandMaturity>,
}
impl Args {
fn parse() -> Result<Self, String> {
let mut args = std::env::args().skip(1);
let mut out = Args {
json: false,
histogram: false,
topo: false,
gate_filter: None,
domain_filter: None,
flake_lock: None,
narinfo_path: None,
registry_resolve: None,
realisation_path: None,
hash_decode: None,
store_path: None,
coverage: false,
coverage_filter: None,
};
while let Some(arg) = args.next() {
match arg.as_str() {
"--json" => out.json = true,
"--histogram" => out.histogram = true,
"--topo" => out.topo = true,
"--gate" => out.gate_filter = Some(args.next()
.ok_or("--gate needs value")?),
"--domain" => out.domain_filter = Some(args.next()
.ok_or("--domain needs value")?),
"--flake-lock" => out.flake_lock = Some(args.next()
.ok_or("--flake-lock needs <path>")?),
"--narinfo" => out.narinfo_path = Some(args.next()
.ok_or("--narinfo needs <path>")?),
"--registry-resolve" => out.registry_resolve = Some(args.next()
.ok_or("--registry-resolve needs <ref>")?),
"--realisation" => out.realisation_path = Some(args.next()
.ok_or("--realisation needs <path>")?),
"--hash-decode" => out.hash_decode = Some(args.next()
.ok_or("--hash-decode needs <hash-string>")?),
"--store-path" => out.store_path = Some(args.next()
.ok_or("--store-path needs </nix/store/...>")?),
"--coverage" => out.coverage = true,
"--coverage-only" => {
out.coverage = true;
let v = args.next().ok_or("--coverage-only needs <maturity>")?;
out.coverage_filter = Some(match v.as_str() {
"working" => SuiCommandMaturity::Working,
"partial" => SuiCommandMaturity::Partial,
"stub" => SuiCommandMaturity::Stub,
"missing" => SuiCommandMaturity::Missing,
"sui-native" => SuiCommandMaturity::SuiNative,
other => return Err(format!(
"--coverage-only takes working|partial|stub|missing|sui-native, got `{other}`"
)),
});
}
"-h" | "--help" => {
print_help();
std::process::exit(0);
}
other => return Err(format!("unknown argument: {other}")),
}
}
Ok(out)
}
}
fn print_help() {
println!(
"sui-spec-inventory — typed substrate introspection.\n\n\
Usage:\n sui-spec-inventory [options]\n\n\
Options:\n \
--json Emit JSON instead of human table\n \
--histogram Emit gate-count summary instead\n \
--topo Sort topologically (leaves first; implementation order)\n \
--gate <name> Filter by maturity gate (Working | M2TypedOnly | \
M3TypedOnly | M4TypedOnly | Informational)\n \
--domain <name> Show one domain (e.g. fetcher, derivation)\n \
--flake-lock <path> Parse a flake.lock and emit a Nord-styled input summary\n \
--narinfo <path> Parse a single .narinfo and emit a Nord-styled record\n \
--registry-resolve <ref> Walk registry precedence (flake-local → user → system → global) for <ref>\n \
--realisation <path> Parse a CA-derivation realisation and show its record\n \
--hash-decode <hash> Classify a hash by algorithm + encoding; decode it\n \
--store-path <path> Decompose a /nix/store path into typed components\n \
--coverage Show sui's nix-replacement coverage matrix\n \
--coverage-only <m> --coverage filtered to one maturity gate\n \
(working | partial | stub | missing | sui-native)\n \
-h, --help This message"
);
}
fn gate_name(gate: MaturityGate) -> String {
match gate {
MaturityGate::Working => "Working".into(),
MaturityGate::M2TypedOnly => "M2TypedOnly".into(),
MaturityGate::M3TypedOnly => "M3TypedOnly".into(),
MaturityGate::M4TypedOnly => "M4TypedOnly".into(),
MaturityGate::Informational => "Informational".into(),
}
}
fn emit_histogram(cat: &[SubstrateDomain]) {
let hist = catalog::maturity_histogram().expect("histogram must compute");
let width = 28;
println!("{}", style::box_top(width, Some("substrate maturity")));
println!(
"{} {:<15} {:>6} {}",
muted("│"),
body("Gate"),
body("Count"),
muted("│"),
);
println!("{}", style::box_mid(width));
for (gate, count) in &hist {
let label = match *gate {
"Working" => success(&format!("{:<15}", gate)),
"M2TypedOnly" | "M3TypedOnly" | "M4TypedOnly" => {
style::pending(&format!("{:<15}", gate))
}
"Informational" => info(&format!("{:<15}", gate)),
_ => body(&format!("{:<15}", gate)),
};
let count_str = body(&format!("{:>6}", count));
println!("{} {} {} {}", muted("│"), label, count_str, muted("│"));
}
println!("{}", style::box_mid(width));
let total = body(&format!("{:>6}", cat.len()));
let total_label = ident(&format!("{:<15}", "Total"));
println!("{} {} {} {}", muted("│"), total_label, total, muted("│"));
println!("{}", style::box_bottom(width));
}
fn gate_style(gate: MaturityGate, text: &str) -> String {
match gate {
MaturityGate::Working => success(text),
MaturityGate::M2TypedOnly => style::warn(text),
MaturityGate::M3TypedOnly | MaturityGate::M4TypedOnly => style::pending(text),
MaturityGate::Informational => info(text),
}
}
fn emit_table(domains: &[&SubstrateDomain]) {
let name_w = domains
.iter()
.map(|d| d.name.len())
.max()
.unwrap_or(10)
.max(6);
let gate_w = 13;
let kw_w = domains
.iter()
.map(|d| d.authoring_keywords.join(", ").len())
.max()
.unwrap_or(20)
.min(40);
let banner = format!(
"{} {} ({} domains)",
glyph_snowflake(),
header("sui-spec substrate"),
ident(&domains.len().to_string()),
);
println!("{banner}");
println!();
println!(
"{} {} {} {}",
body(&format!("{:<name_w$}", "Domain", name_w = name_w)),
body(&format!("{:<gate_w$}", "Gate", gate_w = gate_w)),
body(&format!("{:<kw_w$}", "Keyword(s)", kw_w = kw_w)),
body("Purpose"),
);
println!(
"{}",
muted(&"─".repeat(name_w + gate_w + kw_w + 30))
);
for d in domains {
let kws = d.authoring_keywords.join(", ");
let kw_trunc = if kws.len() > kw_w {
format!("{}…", &kws[..kw_w.saturating_sub(1)])
} else {
kws.clone()
};
let glyph = match d.gate {
MaturityGate::Working => glyph_ok(),
_ => glyph_gear(),
};
let _ = glyph;
println!(
"{} {} {} {} {}",
match d.gate {
MaturityGate::Working => glyph_ok(),
_ => glyph_gear(),
},
ident(&format!("{:<name_w$}", d.name, name_w = name_w - 2)),
gate_style(d.gate, &format!("{:<gate_w$}", gate_name(d.gate), gate_w = gate_w)),
dim_fg(NORD15, &format!("{:<kw_w$}", kw_trunc, kw_w = kw_w)),
body(&d.purpose),
);
}
}
#[allow(dead_code)]
fn _unused() {
let _ = (error("x"), NORD13, NORD3, glyph_arrow(), warn("x"), pending("x"), NORD8);
}
fn emit_flake_lock(path: &str) -> Result<(), Box<dyn std::error::Error>> {
let text = std::fs::read_to_string(path)
.map_err(|e| format!("reading {path}: {e}"))?;
let fmt = lock_file::load_canonical()?
.into_iter()
.find(|f| f.name == "cppnix-flake-lock-v7")
.ok_or("missing cppnix-flake-lock-v7 format")?;
let parsed = lock_file::parse(&text, &fmt)?;
let view = FlakeLockView { lock: parsed, path: path.to_string() };
render_view(&view);
Ok(())
}
struct FlakeLockView {
lock: ParsedLockFile,
path: String,
}
impl OperatorView for FlakeLockView {
fn subject(&self) -> &str { &self.path }
fn header_label(&self) -> &str { "flake.lock" }
fn render_body(&self) {
println!(
" {} {}",
body("version:"),
ident(&format!("v{}", self.lock.version)),
);
println!();
let root_inputs = match lock_file::root_inputs(&self.lock) {
Ok(v) => v,
Err(e) => {
eprintln!("{}", error(&format!("error walking root: {e:?}")));
return;
}
};
println!(
"{} {} {}",
glyph_arrow(),
body("root inputs:"),
ident(&root_inputs.len().to_string()),
);
for input_name in &root_inputs {
println!(
" {} {} {}",
success(input_name),
muted("→"),
describe_input(&self.lock, input_name),
);
}
}
fn render_summary(&self) {
let total = self.lock.nodes.len();
let direct = lock_file::root_inputs(&self.lock).map(|v| v.len()).unwrap_or(0);
let transitives = total.saturating_sub(1 + direct);
println!(
"{} {} nodes total ({} direct, {} transitive)",
muted("∑"),
body(&total.to_string()),
ident(&direct.to_string()),
info(&transitives.to_string()),
);
}
}
fn describe_input(lock: &ParsedLockFile, name: &str) -> String {
let Some(node) = lock.nodes.get(name) else {
return muted("(missing in nodes)").to_string();
};
let Some(locked) = node.get("locked").and_then(|v| v.as_object()) else {
return muted("(no locked metadata)").to_string();
};
let kind = locked.get("type").and_then(|v| v.as_str()).unwrap_or("?");
let rev = locked.get("rev").and_then(|v| v.as_str());
let owner = locked.get("owner").and_then(|v| v.as_str());
let repo = locked.get("repo").and_then(|v| v.as_str());
let url = locked.get("url").and_then(|v| v.as_str());
let nar_hash = locked.get("narHash").and_then(|v| v.as_str());
match (kind, owner, repo, url) {
("github", Some(o), Some(r), _) => format!(
"{} {}{}{}",
info("github:"),
body(&format!("{o}/{r}")),
rev.map(|h| format!(" @{}", &h[..8.min(h.len())])).unwrap_or_default(),
nar_hash.map(|h| format!(" {}", muted(&truncate(h, 28))))
.unwrap_or_default(),
),
("git", _, _, Some(u)) => format!(
"{} {}{}",
info("git:"),
body(u),
rev.map(|h| format!(" @{}", &h[..8.min(h.len())])).unwrap_or_default(),
),
("path", _, _, _) => format!(
"{} {}",
info("path:"),
nar_hash.map(|h| muted(&truncate(h, 32)).to_string())
.unwrap_or_else(|| muted("(no narHash)").to_string()),
),
("tarball", _, _, Some(u)) => format!(
"{} {}",
info("tarball:"),
body(u),
),
_ => format!(
"{} {}",
info(&format!("{kind}:")),
nar_hash.map(|h| muted(&truncate(h, 32)).to_string())
.unwrap_or_else(|| muted("(no metadata)").to_string()),
),
}
}
fn truncate(s: &str, n: usize) -> String {
if s.len() <= n {
s.to_string()
} else {
format!("{}…", &s[..n.saturating_sub(1)])
}
}
fn emit_narinfo(path: &str) -> Result<(), Box<dyn std::error::Error>> {
let text = std::fs::read_to_string(path)
.map_err(|e| format!("reading {path}: {e}"))?;
let fmt = narinfo::load_canonical()?
.into_iter()
.find(|f| f.name == "cppnix-narinfo-v1")
.ok_or("missing cppnix-narinfo-v1 format")?;
let parsed = narinfo::parse(&text, &fmt)?;
let view = NarinfoView { rec: parsed, path: path.to_string() };
render_view(&view);
Ok(())
}
struct NarinfoView {
rec: ParsedNarInfo,
path: String,
}
impl OperatorView for NarinfoView {
fn subject(&self) -> &str { &self.path }
fn header_label(&self) -> &str { "narinfo" }
fn render_body(&self) {
let file_size = self.rec.file_size
.map(|n| format!("{n} bytes"));
LabeledTable::new(14)
.kv("StorePath", &self.rec.store_path)
.kv("URL", &self.rec.url)
.kv("Compression", &self.rec.compression)
.kv("NarHash", &self.rec.nar_hash)
.kv("NarSize", &format!("{} bytes", self.rec.nar_size))
.opt("FileHash", self.rec.file_hash.as_deref())
.opt("FileSize", file_size.as_deref())
.opt("Deriver", self.rec.deriver.as_deref())
.opt("System", self.rec.system.as_deref())
.opt("CA", self.rec.ca.as_deref())
.blank()
.section("References", Some(self.rec.references.len()))
.list_items("→", &self.rec.references)
.blank()
.section("Signatures", Some(self.rec.signatures.len()))
.render();
for sig in &self.rec.signatures {
match sig.split_once(':') {
Some((key, val)) => println!(
" {} {}{}{}",
muted("⎷"),
info(key),
muted(":"),
muted(&truncate(val, 32)),
),
None => println!(" {} {}", muted("⎷"), muted(sig)),
}
}
}
}
fn emit_registry_resolve(flake_ref: &str) -> Result<(), Box<dyn std::error::Error>> {
let real = registry::discover_disk_registries()?;
let any_entries = real.iter().any(|(_, e)| !e.is_empty());
let registries: registry::Registries = if any_entries {
real
} else {
demonstration_entries(flake_ref)
};
let view = RegistryResolveView {
flake_ref: flake_ref.to_string(),
registries,
from_disk: any_entries,
};
render_view(&view);
Ok(())
}
struct RegistryResolveView {
flake_ref: String,
registries: registry::Registries,
from_disk: bool,
}
impl OperatorView for RegistryResolveView {
fn subject(&self) -> &str { &self.flake_ref }
fn header_label(&self) -> &str { "registry resolve" }
fn render_body(&self) {
if !self.from_disk {
println!(
" {} {}",
muted("·"),
muted("(no on-disk registries; showing demo entries)"),
);
println!();
}
let mut sorted: Vec<&(RegistryScope, Vec<RegistryEntry>)> =
self.registries.iter().collect();
sorted.sort_by_key(|(scope, _)| scope_precedence(*scope));
let winning_scope = sorted.iter().find_map(|(scope, entries)| {
if entries.iter().any(|e| e.from == self.flake_ref) {
Some(*scope)
} else {
None
}
});
let scope_w = 14;
println!(
" {} {}",
body(&format!("{:<scope_w$}", "Scope", scope_w = scope_w)),
body("Entry"),
);
println!(" {}", muted(&"─".repeat(60)));
for (scope, entries) in &sorted {
let name = scope_name(*scope);
let match_entry = entries.iter().find(|e| e.from == self.flake_ref);
match match_entry {
Some(entry) => {
let wins = Some(*scope) == winning_scope;
let marker = if wins { glyph_ok() } else { "·".to_string() };
println!(
" {} {} {} {} {}{}",
marker,
success(&format!("{:<scope_w$}", name, scope_w = scope_w - 2)),
info(&entry.from),
muted("→"),
ident(&entry.to),
if entry.exact { format!(" {}", muted("[exact]")) } else { String::new() },
);
}
None => {
println!(
" {} {} {}",
muted("·"),
muted(&format!("{:<scope_w$}", name, scope_w = scope_w - 2)),
muted("(no entry)"),
);
}
}
}
}
fn render_summary(&self) {
let mut sorted: Vec<&(RegistryScope, Vec<RegistryEntry>)> =
self.registries.iter().collect();
sorted.sort_by_key(|(scope, _)| scope_precedence(*scope));
let resolved = sorted.iter().find_map(|(scope, entries)| {
entries.iter()
.find(|e| e.from == self.flake_ref)
.map(|e| (*scope, e))
});
match resolved {
Some((scope, entry)) => println!(
" {} resolves to {} via {}",
glyph_arrow(),
success(&entry.to),
ident(scope_name(scope)),
),
None => println!(
" {} {} {} {}",
error("✘"),
muted("no scope maps"),
ident(&self.flake_ref),
muted("— would error: registry-unresolved"),
),
}
}
}
fn demonstration_entries(flake_ref: &str) -> registry::Registries {
vec![
(
RegistryScope::FlakeLocal,
if flake_ref == "self" || flake_ref == "nixpkgs-overlay" {
vec![RegistryEntry {
from: flake_ref.into(),
to: "github:pleme-io/substrate".into(),
exact: true,
}]
} else {
vec![]
},
),
(
RegistryScope::User,
if flake_ref == "nixpkgs" {
vec![RegistryEntry {
from: "nixpkgs".into(),
to: "github:NixOS/nixpkgs/nixos-unstable".into(),
exact: false,
}]
} else {
vec![]
},
),
(
RegistryScope::System,
vec![],
),
(
RegistryScope::Global,
match flake_ref {
"nixpkgs" => vec![RegistryEntry {
from: "nixpkgs".into(),
to: "github:NixOS/nixpkgs".into(),
exact: false,
}],
"home-manager" => vec![RegistryEntry {
from: "home-manager".into(),
to: "github:nix-community/home-manager".into(),
exact: false,
}],
"flake-utils" => vec![RegistryEntry {
from: "flake-utils".into(),
to: "github:numtide/flake-utils".into(),
exact: false,
}],
_ => vec![],
},
),
]
}
#[allow(dead_code)]
fn emit_registry_table(
flake_ref: &str,
registries: ®istry::Registries,
formats: &[registry::RegistryFormat],
) {
let _ = formats; let banner = format!(
"{} {} {}",
glyph_snowflake(),
header("registry resolve"),
ident(flake_ref),
);
println!("{banner}");
println!();
let mut sorted: Vec<&(RegistryScope, Vec<RegistryEntry>)> = registries.iter().collect();
sorted.sort_by_key(|(scope, _)| scope_precedence(*scope));
let mut winning_scope: Option<RegistryScope> = None;
let mut winning_entry: Option<&RegistryEntry> = None;
for (scope, entries) in &sorted {
for entry in entries.iter() {
if entry.from == flake_ref {
winning_scope = Some(*scope);
winning_entry = Some(entry);
break;
}
}
if winning_entry.is_some() {
break;
}
}
let scope_w = 14;
println!(
" {} {}",
body(&format!("{:<scope_w$}", "Scope", scope_w = scope_w)),
body("Entry"),
);
println!(" {}", muted(&"─".repeat(60)));
for (scope, entries) in &sorted {
let name = scope_name(*scope);
let match_entry = entries.iter().find(|e| e.from == flake_ref);
match match_entry {
Some(entry) => {
let wins = Some(*scope) == winning_scope;
let marker = if wins { glyph_ok() } else { "·".to_string() };
println!(
" {} {} {} {} {}{}",
marker,
success(&format!("{:<scope_w$}", name, scope_w = scope_w - 2)),
info(&entry.from),
muted("→"),
ident(&entry.to),
if entry.exact { format!(" {}", muted("[exact]")) } else { String::new() },
);
}
None => {
println!(
" {} {} {}",
muted("·"),
muted(&format!("{:<scope_w$}", name, scope_w = scope_w - 2)),
muted("(no entry)"),
);
}
}
}
println!();
match (winning_scope, winning_entry) {
(Some(scope), Some(entry)) => {
println!(
" {} resolves to {} via {}",
glyph_arrow(),
success(&entry.to),
ident(scope_name(scope)),
);
}
_ => {
println!(
" {} {} {} {}",
error("✘"),
muted("no scope maps"),
ident(flake_ref),
muted("— would error: registry-unresolved"),
);
}
}
}
fn scope_precedence(scope: RegistryScope) -> u32 {
match scope {
RegistryScope::FlakeLocal => 0,
RegistryScope::User => 1,
RegistryScope::System => 2,
RegistryScope::Global => 3,
}
}
fn scope_name(scope: RegistryScope) -> &'static str {
match scope {
RegistryScope::FlakeLocal => "flake-local",
RegistryScope::User => "user",
RegistryScope::System => "system",
RegistryScope::Global => "global",
}
}
fn emit_realisation(path: &str) -> Result<(), Box<dyn std::error::Error>> {
let text = std::fs::read_to_string(path)
.map_err(|e| format!("reading {path}: {e}"))?;
let fmt = realisation::load_canonical()?
.into_iter()
.find(|f| f.name == "cppnix-realisation-v1")
.ok_or("missing cppnix-realisation-v1 format")?;
let parsed = realisation::parse(&text, &fmt)?;
let view = RealisationView { rec: parsed, path: path.to_string() };
render_view(&view);
Ok(())
}
struct RealisationView {
rec: ParsedRealisation,
path: String,
}
impl OperatorView for RealisationView {
fn subject(&self) -> &str { &self.path }
fn header_label(&self) -> &str { "realisation" }
fn render_body(&self) {
LabeledTable::new(14)
.kv("Id", &self.rec.id)
.kv("OutPath", &self.rec.out_path)
.render();
if let Some((drv, out)) = self.rec.id.split_once('!') {
println!(
" {} {}{}{}",
body(&format!("{:>14}", "[drv!out]")),
info(drv),
muted("!"),
success(out),
);
}
LabeledTable::new(14)
.blank()
.section("Signatures", Some(self.rec.signatures.len()))
.render();
for sig in &self.rec.signatures {
match sig.split_once(':') {
Some((key, val)) => println!(
" {} {}{}{}",
muted("⎷"),
info(key),
muted(":"),
muted(&truncate(val, 40)),
),
None => println!(" {} {}", muted("⎷"), muted(sig)),
}
}
LabeledTable::new(14)
.blank()
.section("DependentRels", Some(self.rec.dependent_realisations.len()))
.list_items("→", &self.rec.dependent_realisations)
.render();
}
}
fn emit_hash_decode(input: &str) -> Result<(), Box<dyn std::error::Error>> {
let (algorithm, bytes) = hash::decode_hash(input)?;
let view = HashDecodeView {
input: input.to_string(),
algorithm,
bytes,
};
render_view(&view);
Ok(())
}
struct HashDecodeView {
input: String,
algorithm: String,
bytes: Vec<u8>,
}
impl OperatorView for HashDecodeView {
fn subject(&self) -> &str { &self.input }
fn header_label(&self) -> &str { "hash decode" }
fn render_body(&self) {
let byte_len = self.bytes.len().to_string();
let bit_len = (self.bytes.len() * 8).to_string();
let encoding = detect_encoding(&self.input).unwrap_or("?");
LabeledTable::new(14)
.kv("Algorithm", &self.algorithm)
.kv("ByteLength", &byte_len)
.kv("BitLength", &bit_len)
.opt("Encoding", Some(encoding))
.blank()
.section("equivalent encodings:", None)
.render();
for target in ["base16", "sri"] {
match hash::encode_hash(&self.algorithm, target, &self.bytes) {
Ok(s) => println!(
" {} {} {}",
info(&format!("{target:>6}")),
muted("→"),
ident(&truncate(&s, 72)),
),
Err(_) => println!(
" {} {}",
info(&format!("{target:>6}")),
muted("(re-encode unsupported)"),
),
}
}
}
fn render_summary(&self) {
println!(
" {} {} {} {} bits via {}",
glyph_arrow(),
success("decoded"),
ident(&self.bytes.len().to_string()),
muted("×8 ="),
info(&self.algorithm),
);
}
}
fn detect_encoding(input: &str) -> Option<&'static str> {
if input.contains('-') && input.split_once('-').is_some_and(|(a, _)| {
matches!(a, "sha256" | "sha512" | "sha1" | "md5")
}) {
return Some("sri");
}
if let Some((alg, suffix)) = input.split_once(':') {
if matches!(alg, "sha256" | "sha512" | "sha1" | "md5") {
return Some(if suffix.chars().all(|c| c.is_ascii_hexdigit()) {
"hex"
} else {
"nix-base32"
});
}
}
if input.chars().all(|c| c.is_ascii_hexdigit()) {
return Some("hex");
}
None
}
fn emit_store_path(path: &str) -> Result<(), Box<dyn std::error::Error>> {
let layouts = store_layout::load_canonical()?;
let mut last_err = None;
for layout in &layouts {
match store_layout::parse_path(layout, path) {
Ok(parsed) => {
let view = StorePathView {
path: path.to_string(),
layout_name: layout.name.clone(),
parsed,
};
render_view(&view);
return Ok(());
}
Err(e) => last_err = Some(e),
}
}
Err(format!(
"no canonical store layout parsed `{path}`: {:?}",
last_err,
).into())
}
struct StorePathView {
path: String,
layout_name: String,
parsed: ParsedStorePath,
}
impl OperatorView for StorePathView {
fn subject(&self) -> &str { &self.path }
fn header_label(&self) -> &str { "store path" }
fn render_body(&self) {
LabeledTable::new(14)
.kv("Layout", &self.layout_name)
.opt("Algorithm", self.parsed.algorithm.as_deref())
.kv("Hash", &self.parsed.hash)
.kv("Name", &self.parsed.name)
.opt("SubPath", self.parsed.sub_path.as_deref())
.render();
let hash_len = self.parsed.hash.len();
let len_note = if hash_len == 32 {
success("32-char nix-base32 (cppnix)")
} else {
warn(&format!("{hash_len}-char (non-standard)"))
};
println!(
" {} {}",
body(&format!("{:>14}", "(hash check)")),
len_note,
);
}
fn render_summary(&self) {
let parts = match (
self.parsed.algorithm.as_deref(),
self.parsed.sub_path.as_deref(),
) {
(Some(a), Some(s)) => format!(
"{}{}{} {} {} {} {}",
info(a), muted(":"), success(&self.parsed.hash),
muted("+"), ident(&self.parsed.name),
muted("/"), ident(s),
),
(Some(a), None) => format!(
"{}{}{} {} {}",
info(a), muted(":"), success(&self.parsed.hash),
muted("+"), ident(&self.parsed.name),
),
(None, Some(s)) => format!(
"{} {} {} {} {}",
success(&self.parsed.hash),
muted("+"), ident(&self.parsed.name),
muted("/"), ident(s),
),
(None, None) => format!(
"{} {} {}",
success(&self.parsed.hash),
muted("+"), ident(&self.parsed.name),
),
};
println!(" {} {}", glyph_arrow(), parts);
}
}
fn emit_coverage(args: &Args) -> Result<(), Box<dyn std::error::Error>> {
let cat = cli_coverage::load_canonical()?;
let hist = cli_coverage::maturity_histogram()?;
let pct = cli_coverage::replacement_percentage()?;
println!(
"{} {} ({} commands)",
glyph_snowflake(),
header("sui nix-replacement coverage"),
ident(&cat.len().to_string()),
);
println!();
let bar_w = 40;
let filled = ((pct * bar_w as f64) as usize).min(bar_w);
let bar: String = (0..bar_w)
.map(|i| if i < filled { '█' } else { '░' })
.collect();
let bar_color = if pct >= 0.7 {
success(&bar)
} else if pct >= 0.4 {
style::pending(&bar)
} else {
warn(&bar)
};
println!(
" {} {} {}",
body("nix replacement:"),
bar_color,
ident(&format!("{:.1}%", pct * 100.0)),
);
println!();
println!(" {}", body("maturity histogram:"));
for (m, count) in &hist {
let row = format!("{:<12}", m.name());
let styled = maturity_style(*m, &row);
println!(
" {} {} {}",
maturity_glyph(*m),
styled,
ident(&count.to_string()),
);
}
println!();
let filtered: Vec<&SuiCommand> = cat
.iter()
.filter(|c| match args.coverage_filter {
Some(m) => c.maturity == m,
None => true,
})
.collect();
if filtered.is_empty() {
eprintln!("no commands match the filter");
return Ok(());
}
let name_w = filtered.iter().map(|c| c.name.len()).max().unwrap_or(10);
let nix_w = filtered.iter().map(|c| c.nix_equivalent.len()).max()
.unwrap_or(20).min(40);
println!(
" {} {} {} {}",
body(&format!("{:<name_w$}", "sui command", name_w = name_w)),
body(&format!("{:<width$}", "nix equivalent", width = nix_w)),
body("maturity"),
body("substrate"),
);
println!(" {}", muted(&"─".repeat(name_w + nix_w + 32)));
for c in &filtered {
let nix = if c.nix_equivalent.is_empty() {
muted(&format!("{:<width$}", "(sui-native)", width = nix_w))
} else {
ident(&format!("{:<width$}", &c.nix_equivalent, width = nix_w))
};
let m_label = maturity_style(c.maturity, c.maturity.name());
let m_glyph = maturity_glyph(c.maturity);
let subs = if c.substrate.is_empty() {
muted("—").to_string()
} else {
success(&c.substrate.join(", "))
};
println!(
" {} {} {} {} {}",
m_glyph,
ident(&format!("{:<name_w$}", c.name, name_w = name_w.saturating_sub(2))),
nix,
m_label,
subs,
);
}
Ok(())
}
fn maturity_glyph(m: SuiCommandMaturity) -> String {
match m {
SuiCommandMaturity::Working => glyph_ok(),
SuiCommandMaturity::Partial => style::pending("◐"),
SuiCommandMaturity::Stub => style::warn("○"),
SuiCommandMaturity::Missing => error("✘"),
SuiCommandMaturity::SuiNative => info("◆"),
}
}
fn maturity_style(m: SuiCommandMaturity, text: &str) -> String {
match m {
SuiCommandMaturity::Working => success(text),
SuiCommandMaturity::Partial => style::pending(text),
SuiCommandMaturity::Stub => style::warn(text),
SuiCommandMaturity::Missing => error(text),
SuiCommandMaturity::SuiNative => info(text),
}
}