use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use super::Config;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct Loadout {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kit: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub settings: Option<LoadoutSettings>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct LoadoutSettings {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub num_ctx: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub framing: Option<String>,
}
impl Loadout {
pub fn validate(&self, cfg: &Config) -> std::result::Result<(), String> {
if let Some(kit) = &self.kit {
cfg.resolve_bundle(kit)
.map_err(|e| format!("loadout kit '{kit}': {e}"))?;
}
if let Some(profile) = &self.profile {
cfg.resolve_profile(profile)
.map_err(|e| format!("loadout profile '{profile}': {e}"))?;
}
if let Some(provider) = &self.provider {
if !cfg.backends.iter().any(|b| &b.name == provider) {
let known = if cfg.backends.is_empty() {
"none defined".to_string()
} else {
cfg.backends
.iter()
.map(|b| b.name.as_str())
.collect::<Vec<_>>()
.join(", ")
};
return Err(format!(
"loadout provider '{provider}': no [backends] entry named '{provider}' (known: {known})"
));
}
}
Ok(())
}
}
impl Config {
pub(super) fn merge_disk_loadouts(&mut self) {
if let Some(dir) = Self::user_config_dir() {
self.merge_loadouts_from_dir(&dir.join("loadouts"));
}
if let Some(proj) = Self::project_config_path() {
if let Some(parent) = proj.parent() {
self.merge_loadouts_from_dir(&parent.join("loadouts"));
}
}
}
pub(super) fn merge_loadouts_from_dir(&mut self, dir: &Path) {
let Ok(entries) = std::fs::read_dir(dir) else {
return; };
let mut paths: Vec<PathBuf> = entries
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().is_some_and(|x| x == "toml"))
.collect();
paths.sort();
for path in paths {
let Some(stem) = path.file_stem().and_then(|s| s.to_str()) else {
continue;
};
match std::fs::read_to_string(&path).map(|t| toml::from_str::<Loadout>(&t)) {
Ok(Ok(loadout)) => {
self.loadouts.insert(stem.to_string(), loadout);
}
Ok(Err(e)) => {
tracing::warn!(path = %path.display(), error = %e, "skipping malformed loadout file");
}
Err(_) => {}
}
}
}
}