chore: fmt

Signed-off-by: AlexsJones <alexsimonjones@gmail.com>
This commit is contained in:
AlexsJones 2026-02-18 21:16:08 +00:00
parent 4d1542b582
commit 7f35522350
6 changed files with 111 additions and 36 deletions

1
.gitignore vendored
View file

@ -1,2 +1,3 @@
/target
llmfit
/docs

View file

@ -31,6 +31,10 @@ Example of a medium performance home laptop
Example of models with Mixture-of-Experts architectures
![moe](moe.png)
Downloading a model via Ollama integration
![download](download.gif)
---
## Install
@ -88,6 +92,10 @@ Launches the interactive terminal UI. Your system specs (CPU, RAM, GPU name, VRA
| `Esc` or `Enter` | Exit search mode |
| `Ctrl-U` | Clear search |
| `f` | Cycle fit filter: All, Runnable, Perfect, Good, Marginal |
| `p` | Open provider filter popup |
| `i` | Toggle installed-first sorting (Ollama only) |
| `d` | Pull/download selected model via Ollama |
| `r` | Refresh installed models from Ollama |
| `1`-`9` | Toggle provider visibility |
| `Enter` | Toggle detail view for selected model |
| `PgUp` / `PgDn` | Scroll by 10 |
@ -225,6 +233,7 @@ src/
hardware.rs -- System RAM/CPU/GPU detection (multi-GPU, backend identification)
models.rs -- Model database, quantization hierarchy, dynamic quant selection
fit.rs -- Multi-dimensional scoring (Q/S/F/C), speed estimation, MoE offloading
providers.rs -- Runtime provider integration (Ollama), model install detection, pull/download
display.rs -- Classic CLI table rendering + JSON output
tui_app.rs -- TUI application state, filters, navigation
tui_ui.rs -- TUI rendering (ratatui)
@ -288,11 +297,36 @@ cargo publish
| `serde` / `serde_json` | JSON deserialization for model database |
| `tabled` | CLI table formatting |
| `colored` | CLI colored output |
| `ureq` | HTTP client for Ollama API integration |
| `ratatui` | Terminal UI framework |
| `crossterm` | Terminal input/output backend for ratatui |
---
## Ollama integration
llmfit integrates with [Ollama](https://ollama.com) to detect which models you already have installed and to download new ones directly from the TUI.
### Requirements
- **Ollama must be installed and running** (`ollama serve` or the Ollama desktop app)
- llmfit connects to `http://localhost:11434` (Ollama's default API port)
- No configuration needed — if Ollama is running, llmfit detects it automatically
### How it works
On startup, llmfit queries `GET /api/tags` to list your installed Ollama models. Each installed model gets a green **✓** in the **Inst** column of the TUI. The system bar shows `Ollama: ✓ (N installed)`.
When you press `d` on a model, llmfit sends `POST /api/pull` to Ollama to download it. The row highlights with an animated progress indicator showing download progress in real-time. Once complete, the model is immediately available for use with Ollama.
If Ollama is not running, the `d`, `i`, and `r` keybindings are hidden from the status bar and disabled — the TUI works normally without Ollama, you just can't see install status or pull models.
### Model name mapping
llmfit's database uses HuggingFace model names (e.g. `Qwen/Qwen2.5-Coder-14B-Instruct`) while Ollama uses its own naming scheme (e.g. `qwen2.5-coder:14b`). llmfit maintains an accurate mapping table between the two so that install detection and pulls resolve to the correct model. Each mapping is exact — `qwen2.5-coder:14b` maps to the Coder model, not the base `qwen2.5:14b`.
---
## Platform support
- **Linux** -- Full support. GPU detection via `nvidia-smi` (NVIDIA), `rocm-smi` (AMD), and sysfs/`lspci` (Intel Arc).

View file

@ -49,7 +49,7 @@ pub struct ModelFit {
pub estimated_tps: f64, // estimated tokens per second
pub best_quant: String, // best quantization for this hardware
pub use_case: UseCase, // inferred use case category
pub installed: bool, // model found in a local runtime provider
pub installed: bool, // model found in a local runtime provider
}
impl ModelFit {

View file

@ -35,7 +35,10 @@ pub struct PullHandle {
#[derive(Debug, Clone)]
pub enum PullEvent {
Progress { status: String, percent: Option<f64> },
Progress {
status: String,
percent: Option<f64>,
},
Done,
Error(String),
}
@ -193,11 +196,7 @@ pub fn hf_name_to_ollama_candidates(hf_name: &str) -> Vec<String> {
let mut candidates = Vec::new();
// Take the part after the slash (repo name)
let repo = hf_name
.split('/')
.last()
.unwrap_or(hf_name)
.to_lowercase();
let repo = hf_name.split('/').last().unwrap_or(hf_name).to_lowercase();
// Common provider-specific mappings from HF repo names → Ollama tags.
// These are checked first since they're authoritative.
@ -282,7 +281,10 @@ pub fn hf_name_to_ollama_candidates(hf_name: &str) -> Vec<String> {
("granite-4.0-h-small", "granite4.0-h:small"),
("zephyr-7b-beta", "zephyr:7b"),
("c4ai-command-r-v01", "command-r"),
("nous-hermes-2-mixtral-8x7b-dpo", "nous-hermes2-mixtral:8x7b"),
(
"nous-hermes-2-mixtral-8x7b-dpo",
"nous-hermes2-mixtral:8x7b",
),
("nomic-embed-text-v1.5", "nomic-embed-text"),
("bge-large-en-v1.5", "bge-large"),
];
@ -317,13 +319,10 @@ pub fn is_model_installed(hf_name: &str, installed: &HashSet<String>) -> bool {
/// Given an HF model name, return the best Ollama tag to use for pulling.
pub fn ollama_pull_tag(hf_name: &str) -> String {
let candidates = hf_name_to_ollama_candidates(hf_name);
candidates.into_iter().next().unwrap_or_else(|| {
hf_name
.split('/')
.last()
.unwrap_or(hf_name)
.to_lowercase()
})
candidates
.into_iter()
.next()
.unwrap_or_else(|| hf_name.split('/').last().unwrap_or(hf_name).to_lowercase())
}
#[cfg(test)]
@ -339,7 +338,10 @@ mod tests {
installed.insert("qwen2.5-coder:14b".to_string());
installed.insert("qwen2.5-coder".to_string());
assert!(is_model_installed("Qwen/Qwen2.5-Coder-14B-Instruct", &installed));
assert!(is_model_installed(
"Qwen/Qwen2.5-Coder-14B-Instruct",
&installed
));
// Must NOT match the non-coder model
assert!(!is_model_installed("Qwen/Qwen2.5-14B-Instruct", &installed));
}
@ -353,7 +355,10 @@ mod tests {
installed.insert("qwen2.5".to_string());
assert!(is_model_installed("Qwen/Qwen2.5-14B-Instruct", &installed));
assert!(!is_model_installed("Qwen/Qwen2.5-Coder-14B-Instruct", &installed));
assert!(!is_model_installed(
"Qwen/Qwen2.5-Coder-14B-Instruct",
&installed
));
}
#[test]
@ -376,7 +381,8 @@ mod tests {
#[test]
fn test_deepseek_coder_mapping() {
let candidates = hf_name_to_ollama_candidates("deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct");
let candidates =
hf_name_to_ollama_candidates("deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct");
assert!(candidates.contains(&"deepseek-coder-v2:16b".to_string()));
}
}

View file

@ -1,7 +1,7 @@
use crate::fit::{FitLevel, ModelFit};
use crate::hardware::SystemSpecs;
use crate::models::ModelDatabase;
use crate::providers::{self, OllamaProvider, ModelProvider, PullHandle, PullEvent};
use crate::providers::{self, ModelProvider, OllamaProvider, PullEvent, PullHandle};
use std::collections::HashSet;
use std::sync::mpsc;
@ -341,7 +341,9 @@ impl App {
if self.pull_active.is_some() {
return; // already pulling
}
let Some(fit) = self.selected_fit() else { return };
let Some(fit) = self.selected_fit() else {
return;
};
if fit.installed {
self.pull_status = Some("Already installed".to_string());
return;
@ -366,7 +368,9 @@ impl App {
if self.pull_active.is_some() {
self.tick_count = self.tick_count.wrapping_add(1);
}
let Some(handle) = &self.pull_active else { return };
let Some(handle) = &self.pull_active else {
return;
};
// Drain all available events
loop {
match handle.receiver.try_recv() {

View file

@ -85,7 +85,11 @@ fn draw_system_bar(frame: &mut Frame, app: &App, area: Rect) {
} else {
"Ollama: ✗".to_string()
};
let ollama_color = if app.ollama_available { Color::Green } else { Color::DarkGray };
let ollama_color = if app.ollama_available {
Color::Green
} else {
Color::DarkGray
};
let text = Line::from(vec![
Span::styled(" CPU: ", Style::default().fg(Color::DarkGray)),
@ -265,8 +269,8 @@ fn pull_indicator(percent: Option<f64>, tick: u64) -> String {
fn draw_table(frame: &mut Frame, app: &mut App, area: Rect) {
let header_cells = [
"", "Inst", "Model", "Provider", "Params", "Score", "tok/s", "Quant", "Mode", "Mem %", "Ctx",
"Fit", "Use Case",
"", "Inst", "Model", "Provider", "Params", "Score", "tok/s", "Quant", "Mode", "Mem %",
"Ctx", "Fit", "Use Case",
]
.iter()
.map(|h| {
@ -479,7 +483,10 @@ fn draw_detail(frame: &mut Frame, app: &App, area: Rect) {
if fit.installed {
Span::styled("✓ Yes (Ollama)", Style::default().fg(Color::Green).bold())
} else if app.ollama_available {
Span::styled("✗ No (press d to pull)", Style::default().fg(Color::DarkGray))
Span::styled(
"✗ No (press d to pull)",
Style::default().fg(Color::DarkGray),
)
} else {
Span::styled("- Ollama not running", Style::default().fg(Color::DarkGray))
},
@ -814,9 +821,17 @@ fn draw_status_bar(frame: &mut Frame, app: &App, area: Rect) {
let (keys, mode_text) = match app.input_mode {
InputMode::Normal => {
let detail_key = if app.show_detail { "Enter:table" } else { "Enter:detail" };
let detail_key = if app.show_detail {
"Enter:table"
} else {
"Enter:detail"
};
let ollama_keys = if app.ollama_available {
let installed_key = if app.installed_first { "i:all" } else { "i:installed↑" };
let installed_key = if app.installed_first {
"i:all"
} else {
"i:installed↑"
};
format!(" {} d:pull r:refresh", installed_key)
} else {
String::new()
@ -824,13 +839,15 @@ fn draw_status_bar(frame: &mut Frame, app: &App, area: Rect) {
(
format!(
" ↑↓/jk:nav {} /:search f:fit{} p:providers q:quit",
detail_key,
ollama_keys,
detail_key, ollama_keys,
),
"NORMAL",
)
}
InputMode::Search => (" Type to search Esc:done Ctrl-U:clear".to_string(), "SEARCH"),
InputMode::Search => (
" Type to search Esc:done Ctrl-U:clear".to_string(),
"SEARCH",
),
InputMode::ProviderPopup => (
" ↑↓/jk:navigate Space:toggle a:all/none Esc:close".to_string(),
"PROVIDERS",
@ -840,7 +857,10 @@ fn draw_status_bar(frame: &mut Frame, app: &App, area: Rect) {
// Split into two lines: keys + progress
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Min(20), Constraint::Length(progress_text.len() as u16 + 2)])
.constraints([
Constraint::Min(20),
Constraint::Length(progress_text.len() as u16 + 2),
])
.split(area);
let status_line = Line::from(vec![
@ -852,9 +872,16 @@ fn draw_status_bar(frame: &mut Frame, app: &App, area: Rect) {
]);
frame.render_widget(Paragraph::new(status_line), chunks[0]);
let pull_color = if app.pull_active.is_some() { Color::Yellow } else { Color::Green };
let pull_color = if app.pull_active.is_some() {
Color::Yellow
} else {
Color::Green
};
frame.render_widget(
Paragraph::new(Line::from(Span::styled(progress_text, Style::default().fg(pull_color)))),
Paragraph::new(Line::from(Span::styled(
progress_text,
Style::default().fg(pull_color),
))),
chunks[1],
);
return;
@ -868,7 +895,11 @@ fn draw_status_bar(frame: &mut Frame, app: &App, area: Rect) {
"Enter:detail"
};
let ollama_keys = if app.ollama_available {
let installed_key = if app.installed_first { "i:all" } else { "i:installed↑" };
let installed_key = if app.installed_first {
"i:all"
} else {
"i:installed↑"
};
format!(" {} d:pull r:refresh", installed_key)
} else {
String::new()
@ -876,8 +907,7 @@ fn draw_status_bar(frame: &mut Frame, app: &App, area: Rect) {
(
format!(
" ↑↓/jk:nav {} /:search f:fit{} p:providers q:quit",
detail_key,
ollama_keys,
detail_key, ollama_keys,
),
"NORMAL",
)