use std::path::{Path, PathBuf};
#[derive(Clone, Debug)]
pub struct LibraryEntry {
pub id: String,
pub snapshot_dir: PathBuf,
pub size_bytes: u64,
pub has_weights: bool,
pub model_type: String,
pub quant: String,
pub layers: usize,
pub hidden: usize,
pub heads: usize,
pub experts: usize,
pub context: usize,
pub optimized: bool,
}
fn dir_size(dir: &Path) -> u64 {
let mut total = 0u64;
let Ok(rd) = std::fs::read_dir(dir) else {
return 0;
};
for e in rd.flatten() {
let p = e.path();
if p.is_dir() {
total += dir_size(&p);
} else if let Ok(md) = std::fs::metadata(&p) {
total += md.len();
}
}
total
}
pub fn scan(cache_dir: Option<&Path>) -> Vec<LibraryEntry> {
let Ok(root) = crate::model_resolver::resolve_cache_root(cache_dir) else {
return Vec::new();
};
let Ok(rd) = std::fs::read_dir(&root) else {
return Vec::new();
};
let mut out = Vec::new();
for e in rd.flatten() {
let name = e.file_name().to_string_lossy().to_string();
let Some(mangled) = name.strip_prefix("models--") else {
continue;
};
let id = mangled.replace("--", "/");
let snapshots = e.path().join("snapshots");
let Some(snap) =
crate::model_resolver::find_snapshot_with_weights(&snapshots).or_else(|| {
std::fs::read_dir(&snapshots)
.ok()?
.flatten()
.map(|s| s.path())
.find(|p| p.is_dir())
})
else {
continue;
};
let has_shard = std::fs::read_dir(&snap)
.map(|rd| {
rd.filter_map(|e| e.ok()).any(|e| {
e.file_name()
.to_str()
.is_some_and(|n| n.ends_with(".safetensors"))
})
})
.unwrap_or(false);
let published = e.path().join("refs/main").exists();
let has_weights = has_shard && published;
let mut entry = LibraryEntry {
id,
size_bytes: match dir_size(&e.path().join("blobs")) {
0 => dir_size(&snap),
n => n,
},
snapshot_dir: snap.clone(),
has_weights,
model_type: "?".into(),
quant: "-".into(),
layers: 0,
hidden: 0,
heads: 0,
experts: 0,
context: 0,
optimized: false,
};
if let Ok(json) = std::fs::read_to_string(snap.join("config.json"))
&& let Ok(cfg) = metrale_config::parse_config(&json)
{
entry.model_type = cfg.model_type.clone();
entry.layers = cfg.num_hidden_layers;
entry.hidden = cfg.hidden_size;
entry.heads = cfg.num_attention_heads;
entry.experts = cfg.num_experts;
entry.context = cfg.max_position_embeddings;
if let Some(q) = &cfg.quantization_config {
entry.quant = if q.quant_algo.is_empty() {
q.quant_method.clone()
} else {
q.quant_algo.to_lowercase()
};
}
entry.optimized = matches!(
metrale_kernels::ptx_for_config(
&cfg.model_type,
cfg.hidden_size,
&[entry.id.as_str()],
None,
),
Ok(Some(_))
);
}
out.push(entry);
}
out.sort_by(|a, b| b.size_bytes.cmp(&a.size_bytes));
out
}
pub fn human_size(bytes: u64) -> String {
crate::tui::format::bytes(bytes)
}
pub fn scan_in_background(
cache_dir: Option<&Path>,
) -> std::sync::mpsc::Receiver<Vec<LibraryEntry>> {
let owned = cache_dir.map(|p| p.to_path_buf());
crate::tui::worker::spawn(
"metrale-libscan",
move || scan(owned.as_deref()),
|_| Vec::new(),
)
}