use std::fs;
use std::path::PathBuf;
use crate::cache::atomic_write;
use crate::error::{AppError, Result};
use crate::vendor::VendorId;
fn state_dir() -> Result<PathBuf> {
let base = directories::BaseDirs::new()
.ok_or_else(|| AppError::Other("could not resolve XDG cache dir".into()))?;
Ok(base.cache_dir().join("ai-usagebar"))
}
fn state_path() -> Result<PathBuf> {
Ok(state_dir()?.join("active_vendor"))
}
pub fn read() -> Option<VendorId> {
let path = state_path().ok()?;
let raw = fs::read_to_string(&path).ok()?;
parse_slug(raw.trim())
}
pub fn write(vendor: VendorId) -> Result<()> {
let path = state_path()?;
atomic_write(&path, vendor.slug().as_bytes())
}
pub fn cycle(enabled: &[VendorId], start: VendorId, delta: i32) -> Result<VendorId> {
if enabled.is_empty() {
return Err(AppError::Other("no enabled vendors to cycle".into()));
}
let current = read().filter(|v| enabled.contains(v)).unwrap_or(start);
let cur_idx = enabled.iter().position(|v| *v == current).unwrap_or(0);
let n = enabled.len() as i32;
let next_idx = ((cur_idx as i32 + delta).rem_euclid(n)) as usize;
let next = enabled[next_idx];
write(next)?;
Ok(next)
}
fn parse_slug(s: &str) -> Option<VendorId> {
match s {
"anthropic" => Some(VendorId::Anthropic),
"openai" => Some(VendorId::Openai),
"zai" => Some(VendorId::Zai),
"openrouter" => Some(VendorId::Openrouter),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_slug_round_trip() {
for id in VendorId::all() {
assert_eq!(parse_slug(id.slug()), Some(*id));
}
}
#[test]
fn parse_slug_unknown_returns_none() {
assert!(parse_slug("not-a-vendor").is_none());
assert!(parse_slug("").is_none());
}
#[test]
fn cycle_wraps_forward_and_backward() {
let enabled = [
VendorId::Anthropic,
VendorId::Openai,
VendorId::Zai,
VendorId::Openrouter,
];
let step = |from: usize, delta: i32| -> VendorId {
enabled[((from as i32 + delta).rem_euclid(4)) as usize]
};
assert_eq!(step(0, 1), VendorId::Openai);
assert_eq!(step(0, -1), VendorId::Openrouter);
assert_eq!(step(3, 1), VendorId::Anthropic);
}
}