use crate::recipe::Recipe;
use crate::tui::data::library::{LibraryEntry, human_size};
#[derive(Clone, Debug)]
pub struct Entry {
pub model: String,
pub recipes: Vec<Recipe>,
pub local: Option<LibraryEntry>,
}
impl Entry {
pub fn has_recipe(&self) -> bool {
!self.recipes.is_empty()
}
pub fn primary(&self) -> Option<&Recipe> {
self.recipes
.iter()
.find(|r| r.is_metrale())
.or_else(|| self.recipes.first())
}
pub fn has_weights(&self) -> bool {
self.local.as_ref().is_some_and(|l| l.has_weights)
}
pub fn optimized(&self) -> bool {
self.local.as_ref().is_some_and(|l| l.optimized)
}
pub fn runnable_now(&self) -> bool {
self.has_weights() && self.recipes.iter().any(Recipe::is_metrale)
}
fn rank(&self) -> u8 {
match (self.runnable_now(), self.has_recipe(), self.has_weights()) {
(true, _, _) => 0,
(_, true, _) => 1,
_ => 2,
}
}
pub fn size_text(&self) -> String {
match &self.local {
Some(l) if l.has_weights => human_size(l.size_bytes),
Some(_) => "partial".into(),
None => "—".into(),
}
}
pub fn subtitle(&self) -> String {
let mut parts: Vec<String> = Vec::new();
if let Some(r) = self.primary() {
if !r.model_params.is_empty() {
parts.push(r.model_params.clone());
}
if !r.quantization.is_empty() {
parts.push(r.quantization.clone());
}
if !r.category.is_empty() {
parts.push(r.category.clone());
}
}
if let Some(l) = &self.local {
if !l.model_type.is_empty() {
parts.push(l.model_type.clone());
}
if l.layers > 0 {
parts.push(format!("{}L", l.layers));
}
}
parts.dedup();
parts.join(" · ")
}
pub fn matches(&self, needle: &str) -> bool {
if needle.is_empty() {
return true;
}
let needle = needle.to_lowercase();
let hay = [
self.model.to_lowercase(),
self.recipes
.iter()
.map(|r| r.id.to_lowercase())
.collect::<Vec<_>>()
.join(" "),
self.local
.as_ref()
.map(|l| l.model_type.to_lowercase())
.unwrap_or_default(),
];
hay.iter().any(|h| h.contains(&needle))
}
}
pub fn join(recipes: &[Recipe], local: &[LibraryEntry]) -> Vec<Entry> {
let mut rows: Vec<Entry> = Vec::new();
let mut by_model: std::collections::BTreeMap<&str, Vec<Recipe>> =
std::collections::BTreeMap::new();
for recipe in recipes {
by_model
.entry(recipe.model.as_str())
.or_default()
.push(recipe.clone());
}
for (model, mut group) in by_model {
group.sort_by(|a, b| a.id.cmp(&b.id));
rows.push(Entry {
model: model.to_string(),
recipes: group,
local: local.iter().find(|l| l.id == model).cloned(),
});
}
for entry in local {
if recipes.iter().any(|r| r.model == entry.id) {
continue;
}
rows.push(Entry {
model: entry.id.clone(),
recipes: Vec::new(),
local: Some(entry.clone()),
});
}
rows.sort_by(|a, b| a.rank().cmp(&b.rank()).then_with(|| a.model.cmp(&b.model)));
rows
}
#[cfg(test)]
#[path = "catalogue_tests.rs"]
mod tests;