1use std::fs;
9use std::path::PathBuf;
10
11use crate::cache::atomic_write;
12use crate::error::{AppError, Result};
13use crate::vendor::VendorId;
14
15fn state_dir() -> Result<PathBuf> {
16 let base = directories::BaseDirs::new()
17 .ok_or_else(|| AppError::Other("could not resolve XDG cache dir".into()))?;
18 Ok(base.cache_dir().join("ai-usagebar"))
19}
20
21fn state_path() -> Result<PathBuf> {
22 Ok(state_dir()?.join("active_vendor"))
23}
24
25pub fn read() -> Option<VendorId> {
28 let path = state_path().ok()?;
29 let raw = fs::read_to_string(&path).ok()?;
30 parse_slug(raw.trim())
31}
32
33pub fn write(vendor: VendorId) -> Result<()> {
35 let path = state_path()?;
36 atomic_write(&path, vendor.slug().as_bytes())
37}
38
39pub fn cycle(enabled: &[VendorId], start: VendorId, delta: i32) -> Result<VendorId> {
43 if enabled.is_empty() {
44 return Err(AppError::Other("no enabled vendors to cycle".into()));
45 }
46 let current = read().filter(|v| enabled.contains(v)).unwrap_or(start);
47 let cur_idx = enabled.iter().position(|v| *v == current).unwrap_or(0);
48 let n = enabled.len() as i32;
49 let next_idx = ((cur_idx as i32 + delta).rem_euclid(n)) as usize;
50 let next = enabled[next_idx];
51 write(next)?;
52 Ok(next)
53}
54
55fn parse_slug(s: &str) -> Option<VendorId> {
56 match s {
57 "anthropic" => Some(VendorId::Anthropic),
58 "openai" => Some(VendorId::Openai),
59 "zai" => Some(VendorId::Zai),
60 "openrouter" => Some(VendorId::Openrouter),
61 "deepseek" => Some(VendorId::Deepseek),
62 _ => None,
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69
70 #[test]
71 fn parse_slug_round_trip() {
72 for id in VendorId::all() {
73 assert_eq!(parse_slug(id.slug()), Some(*id));
74 }
75 }
76
77 #[test]
78 fn parse_slug_unknown_returns_none() {
79 assert!(parse_slug("not-a-vendor").is_none());
80 assert!(parse_slug("").is_none());
81 }
82
83 #[test]
84 fn cycle_wraps_forward_and_backward() {
85 let enabled = [
89 VendorId::Anthropic,
90 VendorId::Openai,
91 VendorId::Zai,
92 VendorId::Openrouter,
93 ];
94 let step = |from: usize, delta: i32| -> VendorId {
95 enabled[((from as i32 + delta).rem_euclid(4)) as usize]
96 };
97 assert_eq!(step(0, 1), VendorId::Openai);
99 assert_eq!(step(0, -1), VendorId::Openrouter);
101 assert_eq!(step(3, 1), VendorId::Anthropic);
103 }
104}