1use std::time::Duration;
4
5use chrono::Utc;
6use reqwest::Client;
7
8use crate::cache::DEFAULT_TTL;
9use crate::config::Config;
10use crate::error::Result;
11use crate::theme::Theme;
12use crate::vendor::{VendorId, VendorOutcome};
13
14#[derive(Debug, Clone)]
20pub enum TabState {
21 Loading,
22 Ready(Box<ReadyTab>),
23 Error(String),
24}
25
26#[derive(Debug, Clone)]
27pub struct ReadyTab {
28 pub snapshot: crate::usage::VendorSnapshot,
29 pub stale: bool,
30 pub last_error: Option<(u16, String)>,
31 pub fetched_at: Option<chrono::DateTime<chrono::Utc>>,
36}
37
38#[derive(Debug)]
39pub struct App {
40 pub vendors: Vec<VendorId>,
41 pub active: usize,
42 pub tabs: Vec<TabState>,
43 pub theme: Theme,
44 pub last_refresh: chrono::DateTime<chrono::Utc>,
45 pub quit: bool,
46 pub settings: Option<crate::tui::settings::SettingsState>,
48}
49
50impl App {
51 pub fn new(vendors: Vec<VendorId>) -> Self {
52 let n = vendors.len();
53 Self {
54 vendors,
55 active: 0,
56 tabs: vec![TabState::Loading; n],
57 theme: Theme::default().merged_with_omarchy(),
58 last_refresh: Utc::now(),
59 quit: false,
60 settings: None,
61 }
62 }
63
64 pub fn new_with_primary(vendors: Vec<VendorId>, primary: Option<VendorId>) -> Self {
68 let mut app = Self::new(vendors);
69 app.select_primary(primary);
70 app
71 }
72
73 pub fn active_vendor(&self) -> Option<VendorId> {
74 self.vendors.get(self.active).copied()
75 }
76
77 pub fn select_primary(&mut self, primary: Option<VendorId>) {
78 if let Some(p) = primary {
79 if let Some(idx) = self.vendors.iter().position(|v| *v == p) {
80 self.active = idx;
81 }
82 }
83 }
84
85 pub fn next_tab(&mut self) {
86 if !self.vendors.is_empty() {
87 self.active = (self.active + 1) % self.vendors.len();
88 }
89 }
90
91 pub fn prev_tab(&mut self) {
92 if !self.vendors.is_empty() {
93 self.active = (self.active + self.vendors.len() - 1) % self.vendors.len();
94 }
95 }
96}
97
98pub async fn refresh_one(client: &Client, config: &Config, vendor: VendorId) -> TabState {
100 match build_outcome(client, config, vendor).await {
101 Ok(outcome) => {
102 let now = Utc::now();
107 let fetched_at = outcome
108 .cache_age
109 .map(|age| now - chrono::Duration::from_std(age).unwrap_or_default());
110 TabState::Ready(Box::new(ReadyTab {
111 snapshot: outcome.snapshot,
112 stale: outcome.stale,
113 last_error: outcome.last_error,
114 fetched_at,
115 }))
116 }
117 Err(e) => TabState::Error(e.to_string()),
118 }
119}
120
121async fn build_outcome(
122 client: &Client,
123 config: &Config,
124 vendor: VendorId,
125) -> Result<VendorOutcome> {
126 match vendor {
127 VendorId::Anthropic => {
128 let cache = crate::cache::Cache::for_vendor("anthropic")?;
129 let creds_path = config
130 .anthropic
131 .credentials_path
132 .clone()
133 .unwrap_or_else(|| crate::anthropic::creds::default_path().unwrap_or_default());
134 let endpoints = crate::anthropic::fetch::Endpoints::default();
135 let outcome = crate::anthropic::fetch_snapshot(
136 client,
137 &creds_path,
138 &cache,
139 &endpoints,
140 DEFAULT_TTL,
141 )
142 .await?;
143 Ok(crate::vendor::VendorOutcome {
144 snapshot: crate::usage::VendorSnapshot::Anthropic(outcome.snapshot),
145 stale: outcome.stale,
146 last_error: outcome.last_error,
147 cache_age: outcome.cache_age,
148 })
149 }
150 VendorId::Openrouter => {
151 let api_key = crate::config::resolve_api_key(
152 "OpenRouter",
153 &config.openrouter.api_key_env,
154 config.openrouter.api_key.as_deref(),
155 )?;
156 let cache = crate::cache::Cache::for_vendor("openrouter")?;
157 let endpoints = crate::openrouter::fetch::Endpoints::default();
158 let outcome = crate::openrouter::fetch_snapshot(
159 client,
160 &api_key,
161 &cache,
162 &endpoints,
163 DEFAULT_TTL,
164 )
165 .await?;
166 Ok(outcome.into())
167 }
168 VendorId::Zai => {
169 let api_key = crate::config::resolve_api_key(
170 "Zai",
171 &config.zai.api_key_env,
172 config.zai.api_key.as_deref(),
173 )?;
174 let cache = crate::cache::Cache::for_vendor("zai")?;
175 let endpoints = crate::zai::fetch::Endpoints::default();
176 let outcome = crate::zai::fetch_snapshot(
177 client,
178 &api_key,
179 &cache,
180 &endpoints,
181 DEFAULT_TTL,
182 config.zai.plan_tier.as_deref(),
183 )
184 .await?;
185 Ok(outcome.into())
186 }
187 VendorId::Openai => {
188 let cache = crate::cache::Cache::for_vendor("openai")?;
189 let creds_path = config
190 .openai
191 .codex_auth_path
192 .clone()
193 .unwrap_or_else(|| crate::openai::creds::default_path().unwrap_or_default());
194 let endpoints = crate::openai::fetch::Endpoints::default();
195 let outcome =
196 crate::openai::fetch_snapshot(client, &creds_path, &cache, &endpoints, DEFAULT_TTL)
197 .await?;
198 Ok(outcome.into())
199 }
200 VendorId::Deepseek => {
201 let api_key = crate::config::resolve_api_key(
202 "DeepSeek",
203 &config.deepseek.api_key_env,
204 config.deepseek.api_key.as_deref(),
205 )?;
206 let cache = crate::cache::Cache::for_vendor("deepseek")?;
207 let endpoints = crate::deepseek::fetch::Endpoints::default();
208 let outcome =
209 crate::deepseek::fetch_snapshot(client, &api_key, &cache, &endpoints, DEFAULT_TTL)
210 .await?;
211 Ok(outcome.into())
212 }
213 }
214}
215
216pub const REFRESH_INTERVAL: Duration = Duration::from_secs(60);
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn select_primary_moves_to_enabled_vendor() {
226 let mut app = App::new(vec![VendorId::Anthropic, VendorId::Openrouter]);
227 app.select_primary(Some(VendorId::Openrouter));
228 assert_eq!(app.active_vendor(), Some(VendorId::Openrouter));
229 }
230
231 #[test]
232 fn select_primary_ignores_disabled_vendor() {
233 let mut app = App::new(vec![VendorId::Anthropic]);
234 app.select_primary(Some(VendorId::Openai));
235 assert_eq!(app.active_vendor(), Some(VendorId::Anthropic));
236 }
237}