use super::types::App;
use std::path::PathBuf;
impl App {
pub fn render<T: ratatui::backend::Backend>(
&mut self,
terminal: &mut ratatui::Terminal<T>,
) -> std::io::Result<()> {
if self.ui.needs_full_redraw {
terminal.clear()?;
self.ui.needs_full_redraw = false;
}
terminal.draw(|frame| crate::tui::render::render(frame, self))?;
Ok(())
}
pub fn discover_models(
dirs: &[PathBuf],
downloads: &[crate::models::DownloadState],
) -> Vec<crate::models::DiscoveredModel> {
let mut models = Vec::new();
let expected_sizes: std::collections::HashMap<&str, u64> = downloads
.iter()
.filter(|d| d.status == crate::models::DownloadStatus::Downloading)
.map(|d| (d.filename.as_str(), d.total_bytes))
.collect();
for dir in dirs {
crate::backend::hub::walk_dir_recursive(dir, 0, 10, &mut |entry| {
let path = entry.path();
if path.is_file()
&& path.extension().map(|e| e == "gguf").unwrap_or(false)
&& let Some(name) = path.file_name().and_then(|n| n.to_str())
{
let name = name.to_string();
let size = entry.metadata().map(|m| m.len()).unwrap_or(0);
if let Some(&expected) = expected_sizes.get(name.as_str())
&& size != expected {
return;
}
let display_name = path
.strip_prefix(dir)
.ok()
.and_then(|p| p.to_str())
.unwrap_or(&name)
.to_string();
models.push(crate::models::DiscoveredModel {
path,
name,
file_size: size,
display_name,
});
}
});
}
models.sort_by(|a, b| a.name.cmp(&b.name));
models
}
pub fn reset_to_defaults(&mut self) {
let defaults = crate::models::ModelSettings::default();
self.settings = defaults;
self.model_settings_cache = self.settings.clone();
self.loading.model_total_layers = 0;
self.loading.model_hidden_size = 0;
self.loading.model_n_ctx_train = 0;
self.loading.model_n_head = 0;
self.loading.model_n_kv_head = 0;
self.loading.vram_estimate = 0;
self.settings.spec_type = String::new();
self.settings.draft_tokens = 0;
self.settings_state.settings_render_cache = None;
self.add_log(
"Reset LLM Settings to defaults",
crate::config::LogLevel::Info,
);
self.ui.needs_redraw = true;
}
pub fn selected_model(&self) -> Option<&crate::models::DiscoveredModel> {
self.selected_model_idx.and_then(|i| self.models.get(i))
}
pub fn selected_model_settings(&self) -> crate::models::ModelSettings {
let model_name = self.selected_model().map(|m| m.display_name.as_str());
self.config.resolve_settings(model_name, None)
}
pub fn on_model_selection_change(&mut self) {
self.search.readme_cache = None;
if let Some(idx) = self.selected_model_idx {
let model = self.models[idx].clone();
self.model_settings_cache = self.selected_model_settings();
self.settings = self.model_settings_cache.clone();
self.update_model_metadata();
self.update_vram_estimate();
if self.is_model_loaded(&model.display_name) {
self.loading.loading_progress = 1.0;
if !self
.loading
.loading_phases
.contains(&super::types::LoadingPhase::Complete)
{
self.loading
.loading_phases
.insert(super::types::LoadingPhase::Complete);
}
} else if matches!(
self.model_states.get(&model.display_name),
Some(crate::models::ModelState::Loading)
| Some(crate::models::ModelState::Benchmarking)
) {
} else {
self.loading.loading_progress = 0.0;
self.loading.loading_phases.clear();
self.loading.last_active_phase = None;
self.loading.load_progress = Default::default();
}
} else {
let default_params = self.config.default.clone();
self.model_settings_cache = default_params.into();
self.loading.model_total_layers = 0;
self.loading.model_hidden_size = 0;
self.loading.model_n_ctx_train = 0;
self.settings.spec_type = String::new();
self.settings.draft_tokens = 0;
self.loading.vram_estimate = 0;
self.loading.loading_progress = 0.0;
self.loading.loading_phases.clear();
self.loading.last_active_phase = None;
}
self.ui.needs_redraw = true;
}
pub fn search_results_len(&self) -> usize {
if let super::types::ModelsMode::Search { results, .. } = &self.models_mode {
results.len()
} else {
0
}
}
pub fn get_filtered_model_indices(&self) -> Vec<usize> {
self.models
.iter()
.enumerate()
.filter(|(_, m)| {
self.search.local_filter.is_empty()
|| m.display_name
.to_lowercase()
.contains(&self.search.local_filter.to_lowercase())
})
.map(|(i, _)| i)
.collect()
}
}
fn normalize_separators(s: &str) -> String {
s.replace('_', "-")
}
pub fn model_is_downloaded(models: &[crate::models::DiscoveredModel], model_id: &str) -> bool {
let repo_name = model_id
.rsplit('/')
.next()
.unwrap_or(model_id)
.to_lowercase();
let repo_normalized = normalize_separators(&repo_name);
let repo_parts: Vec<&str> = repo_normalized.split('-').collect();
models.iter().any(|m| {
let mut local = m.name.to_lowercase();
if let Some(stripped) = local.strip_suffix(".gguf") {
local = stripped.to_string();
}
let local_normalized = normalize_separators(&local);
if local_normalized == repo_normalized {
return true;
}
for i in 1..=repo_parts.len() {
let prefix = repo_parts[..i].join("-");
if prefix.len() >= 8 && local_normalized.starts_with(&format!("{}-", prefix)) {
return true;
}
}
false
})
}
pub fn model_dir_has_contents(models_dirs: &[PathBuf], model_id: &str) -> bool {
for dir in models_dirs {
let model_dir = dir.join(model_id);
if let Ok(mut entries) = std::fs::read_dir(&model_dir) {
return entries.next().is_some();
}
}
false
}