use std::{
env,
fs::{self, File, OpenOptions},
io::{self, BufRead, BufReader, IsTerminal, Read, Write},
net::TcpStream,
path::{Path, PathBuf},
process::{Child, ChildStdin, Command as ProcessCommand, Stdio},
sync::mpsc,
thread,
time::{Duration, Instant},
};
use anyhow::{Context, Result, anyhow, bail};
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use chrono::{DateTime, Local, Utc};
use clap::{Parser, Subcommand};
use crossterm::{
event::{
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind, MouseButton,
MouseEvent, MouseEventKind,
},
execute,
terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
};
use directories::BaseDirs;
use ratatui::{
Frame, Terminal,
backend::{Backend, CrosstermBackend},
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style},
text::{Line, Span, Text},
widgets::{
Block, Borders, Clear, Paragraph, Scrollbar, ScrollbarOrientation, ScrollbarState, Wrap,
},
};
use reqwest::blocking::{Client, Response};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use sha2::{Digest, Sha256};
use thiserror::Error;
const MANAGEMENT_PATH: &str = "/v0/management";
const CODEX_USAGE_URL: &str = "https://chatgpt.com/backend-api/wham/usage";
const CLAUDE_USAGE_URL: &str = "https://api.anthropic.com/api/oauth/usage";
const GOOGLE_LOAD_ASSIST_URL: &str =
"https://daily-cloudcode-pa.googleapis.com/v1internal:loadCodeAssist";
const ANTIGRAVITY_QUOTA_SUMMARY_URL: &str =
"https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary";
const RELEASE_URL: &str = "https://api.github.com/repos/router-for-me/CLIProxyAPI/releases/latest";
const DEFAULT_CPA_URL: &str = "http://127.0.0.1:8317";
const FORMAT_VERSION: u8 = 1;
const DASHBOARD_HEADER_HEIGHT: u16 = 2;
const DASHBOARD_FOOTER_HEIGHT: u16 = 2;
type AppTerminal = Terminal<CrosstermBackend<io::Stdout>>;
#[derive(Parser, Debug)]
#[command(
name = "qota",
version,
about = "Local AI quota dashboard powered by CLIProxyAPI"
)]
struct Cli {
#[arg(long)]
config: Option<PathBuf>,
#[arg(long)]
refresh_interval: Option<u64>,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand, Debug)]
enum Commands {
Setup {
#[arg(long)]
providers: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
},
List {
#[arg(long)]
json: bool,
#[arg(long)]
provider: Option<String>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long, default_value_t = 15_000)]
timeout: u64,
},
Poll {
#[arg(long, default_value_t = 60)]
interval: u64,
#[arg(long)]
once: bool,
#[arg(long)]
background: bool,
#[arg(long)]
json: bool,
#[arg(long)]
provider: Option<String>,
#[arg(long)]
output: Option<PathBuf>,
#[arg(long)]
config: Option<PathBuf>,
#[arg(long, default_value_t = 15_000)]
timeout: u64,
},
}
#[derive(Debug, Error)]
#[error("{provider}: {message}")]
struct ProviderError {
provider: String,
message: String,
}
impl ProviderError {
fn new(provider: impl Into<String>, message: impl Into<String>) -> Self {
Self {
provider: provider.into(),
message: message.into(),
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
struct StoredConfig {
#[serde(alias = "url")]
cpa_url: Option<String>,
management_key: String,
cpa_binary: Option<String>,
cpa_config_path: Option<String>,
#[serde(
default,
alias = "refresh_interval",
alias = "refreshIntervalSeconds",
alias = "refresh_interval_seconds"
)]
refresh_interval: Option<u64>,
}
#[derive(Debug, Clone)]
struct CpaConfig {
url: String,
management_key: String,
binary: Option<PathBuf>,
cpa_config_path: Option<PathBuf>,
refresh_interval: u64,
}
#[derive(Debug, Clone)]
struct Account {
id: String,
label: String,
provider: String,
auth_index: String,
raw: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct QuotaWindow {
key: String,
label: String,
#[serde(skip_serializing_if = "Option::is_none")]
group: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
group_description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
efforts: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
percent_used: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
remaining_percent: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
resets_at: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct AccountSnapshot {
provider: String,
account_id: String,
account_label: String,
fetched_at: String,
windows: Vec<QuotaWindow>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ReportError {
provider: String,
account_id: String,
#[serde(default)]
account_label: String,
message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct UsageReport {
version: u8,
generated_at: String,
accounts: Vec<AccountSnapshot>,
errors: Vec<ReportError>,
}
#[derive(Debug, Clone, Copy)]
struct DownloadProgress {
downloaded: u64,
total: Option<u64>,
stage: &'static str,
}
fn home_dir() -> Result<PathBuf> {
BaseDirs::new()
.map(|dirs| dirs.home_dir().to_path_buf())
.ok_or_else(|| anyhow!("home directory unavailable"))
}
fn qota_dir() -> Result<PathBuf> {
Ok(home_dir()?.join(".qota"))
}
fn config_path(value: Option<PathBuf>) -> Result<PathBuf> {
value
.or_else(|| env::var_os("QOTA_CONFIG").map(PathBuf::from))
.map(Ok)
.unwrap_or_else(|| Ok(qota_dir()?.join("config.json")))
}
fn output_path(value: Option<PathBuf>) -> Result<PathBuf> {
Ok(value.unwrap_or(home_dir()?.join(".podium").join("usage.json")))
}
fn now() -> String {
Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
}
fn color(value: Color) -> Color {
if env::var_os("NO_COLOR").is_some() {
Color::Reset
} else {
value
}
}
fn secure_directory(path: &Path) -> Result<()> {
fs::create_dir_all(path).with_context(|| format!("create {}", path.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(path, fs::Permissions::from_mode(0o700))?;
}
Ok(())
}
fn write_secure(path: &Path, bytes: &[u8]) -> Result<()> {
let parent = path
.parent()
.ok_or_else(|| anyhow!("{} has no parent", path.display()))?;
secure_directory(parent)?;
let temporary = path.with_extension(format!("{}.tmp", std::process::id()));
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&temporary)?;
file.write_all(bytes)?;
file.sync_all()?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&temporary, fs::Permissions::from_mode(0o600))?;
}
fs::rename(temporary, path)?;
Ok(())
}
fn read_cpa_config(path: &Path) -> Result<CpaConfig> {
let stored: StoredConfig = serde_json::from_slice(
&fs::read(path).with_context(|| format!("read {}", path.display()))?,
)
.with_context(|| format!("{}: expected JSON object", path.display()))?;
if stored.management_key.is_empty() {
bail!("qota is not configured; run qota setup");
}
let url = stored
.cpa_url
.unwrap_or_else(|| DEFAULT_CPA_URL.to_string())
.trim_end_matches('/')
.to_string();
if url.is_empty() {
bail!("CLIProxyAPI URL must be a non-empty string");
}
Ok(CpaConfig {
url,
management_key: stored.management_key,
binary: stored.cpa_binary.map(PathBuf::from),
cpa_config_path: stored.cpa_config_path.map(PathBuf::from),
refresh_interval: stored.refresh_interval.unwrap_or(60),
})
}
fn client(timeout_ms: u64) -> Result<Client> {
Client::builder()
.timeout(Duration::from_millis(timeout_ms))
.user_agent("qota/0.2")
.build()
.context("create HTTP client")
}
fn request_json(
_client: &Client,
provider: &str,
request: reqwest::blocking::RequestBuilder,
) -> Result<Value> {
let response = request
.send()
.map_err(|error| ProviderError::new(provider, error.to_string()))?;
let status = response.status();
let text = response
.text()
.map_err(|error| ProviderError::new(provider, error.to_string()))?;
let value: Value = serde_json::from_str(&text)
.map_err(|_| ProviderError::new(provider, format!("invalid JSON response ({status})")))?;
if !status.is_success() {
let message = value
.pointer("/error/message")
.and_then(Value::as_str)
.or_else(|| value.get("message").and_then(Value::as_str))
.unwrap_or(&format!("request failed ({status})"))
.to_string();
bail!(ProviderError::new(provider, message));
}
Ok(value)
}
fn management_request(
client: &Client,
cpa: &CpaConfig,
path: &str,
body: Option<Value>,
) -> Result<Value> {
let url = format!("{}{MANAGEMENT_PATH}{path}", cpa.url);
let request = match body {
Some(value) => client
.post(url)
.bearer_auth(&cpa.management_key)
.json(&value),
None => client.get(url).bearer_auth(&cpa.management_key),
};
request_json(client, "cliproxyapi", request)
}
fn string_at(value: &Value, paths: &[&str]) -> Option<String> {
paths.iter().find_map(|path| {
value
.pointer(path)
.and_then(Value::as_str)
.map(ToString::to_string)
})
}
fn normalize_accounts(payload: Value) -> Result<Vec<Account>> {
let files = payload
.get("files")
.and_then(Value::as_array)
.or_else(|| payload.as_array())
.ok_or_else(|| {
ProviderError::new(
"cliproxyapi",
"auth-files response contained no files array",
)
})?;
Ok(files
.iter()
.enumerate()
.map(|(index, raw)| {
let id = string_at(raw, &["/auth_index", "/authIndex", "/id", "/name"])
.unwrap_or_else(|| format!("account-{index}"));
let label = string_at(raw, &["/label", "/email", "/name", "/account"])
.unwrap_or_else(|| id.clone());
let provider = string_at(raw, &["/provider", "/type"])
.unwrap_or_default()
.to_lowercase();
let provider = match provider.as_str() {
"gemini" | "gemini-cli" => "gemini-cli".to_string(),
"anthropic" | "claude-code" => "claude".to_string(),
_ => provider,
};
let auth_index =
string_at(raw, &["/auth_index", "/authIndex"]).unwrap_or_else(|| id.clone());
Account {
id,
label,
provider,
auth_index,
raw: raw.clone(),
}
})
.collect())
}
fn api_call(
client: &Client,
cpa: &CpaConfig,
account: &Account,
method: &str,
url: &str,
headers: Value,
data: Option<String>,
) -> Result<Value> {
let mut request = json!({ "auth_index": account.auth_index, "method": method, "url": url, "header": headers });
if let Some(data) = data {
request["data"] = Value::String(data);
}
let response = management_request(client, cpa, "/api-call", Some(request))?;
let status = response
.get("status_code")
.or_else(|| response.get("statusCode"))
.and_then(Value::as_u64)
.ok_or_else(|| {
ProviderError::new(
&account.provider,
"CLIProxyAPI api-call response missing status_code",
)
})?;
let mut body = response.get("body").cloned().unwrap_or_else(|| json!({}));
if let Some(text) = body.as_str() {
body = serde_json::from_str(text).map_err(|_| {
ProviderError::new(
&account.provider,
format!("CLIProxyAPI api-call returned invalid JSON ({status})"),
)
})?;
}
if !(200..300).contains(&status) {
let message = body
.pointer("/error/message")
.and_then(Value::as_str)
.or_else(|| body.get("message").and_then(Value::as_str))
.or_else(|| body.get("error").and_then(Value::as_str))
.map(ToString::to_string)
.unwrap_or_else(|| format!("upstream request failed ({status})"));
bail!(ProviderError::new(&account.provider, message));
}
Ok(body)
}
fn json_number(value: Option<&Value>) -> Option<f64> {
value
.and_then(Value::as_f64)
.filter(|number| number.is_finite())
}
fn json_iso(value: Option<&Value>) -> Option<String> {
let value = value?;
let milliseconds = value.as_i64().map(|number| {
if number < 1_000_000_000_000 {
number * 1000
} else {
number
}
});
if let Some(ms) = milliseconds {
return DateTime::<Utc>::from_timestamp_millis(ms)
.map(|date| date.to_rfc3339_opts(chrono::SecondsFormat::Millis, true));
}
value
.as_str()
.and_then(|text| DateTime::parse_from_rfc3339(text).ok())
.map(|date| {
date.to_utc()
.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
})
}
fn clamp(value: f64) -> f64 {
value.clamp(0.0, 100.0)
}
fn quota_window(
key: impl Into<String>,
label: impl Into<String>,
group: Option<String>,
efforts: Vec<String>,
remaining: Option<f64>,
used: Option<f64>,
reset: Option<String>,
) -> QuotaWindow {
let percent_used = used
.or_else(|| remaining.map(|value| 100.0 - value))
.map(clamp);
let remaining_percent = remaining
.or_else(|| percent_used.map(|value| 100.0 - value))
.map(clamp);
QuotaWindow {
key: key.into(),
label: label.into(),
group,
group_description: None,
efforts: (!efforts.is_empty()).then_some(efforts),
percent_used,
remaining_percent,
resets_at: reset,
}
}
fn parse_codex_usage(payload: &Value) -> Vec<QuotaWindow> {
let rate = payload
.get("rate_limit")
.or_else(|| payload.get("rateLimit"))
.unwrap_or(payload);
[
(
"5h",
"5 hour",
rate.get("primary_window")
.or_else(|| rate.get("primaryWindow"))
.or_else(|| rate.get("primary")),
),
(
"7d",
"7 day",
rate.get("secondary_window")
.or_else(|| rate.get("secondaryWindow"))
.or_else(|| rate.get("secondary")),
),
]
.into_iter()
.filter_map(|(fallback_key, fallback_label, source)| {
source
.filter(|source| {
source.get("used_percent").is_some()
|| source.get("usedPercent").is_some()
|| source.get("utilization").is_some()
|| source.get("remaining_percent").is_some()
|| source.get("remainingPercent").is_some()
|| source.get("reset_at").is_some()
|| source.get("resets_at").is_some()
|| source.get("resetAt").is_some()
})
.map(|source| {
let seconds = json_number(
source
.get("limit_window_seconds")
.or_else(|| source.get("limitWindowSeconds")),
);
let (key, label) = match seconds {
Some(value) if value >= 6.0 * 24.0 * 3600.0 => ("7d", "7 day"),
Some(value) if (4.0 * 3600.0..=6.0 * 3600.0).contains(&value) => {
("5h", "5 hour")
}
_ => (fallback_key, fallback_label),
};
quota_window(
key,
label,
None,
vec![],
json_number(
source
.get("remaining_percent")
.or_else(|| source.get("remainingPercent")),
),
json_number(
source
.get("used_percent")
.or_else(|| source.get("usedPercent"))
.or_else(|| source.get("utilization")),
),
json_iso(
source
.get("reset_at")
.or_else(|| source.get("resets_at"))
.or_else(|| source.get("resetAt")),
),
)
})
})
.collect()
}
fn parse_claude_usage(payload: &Value) -> Vec<QuotaWindow> {
[
("5h", "5 hour", "five_hour"),
("7d", "7 day", "seven_day"),
("7d-opus", "7 day Opus", "seven_day_opus"),
("7d-sonnet", "7 day Sonnet", "seven_day_sonnet"),
]
.into_iter()
.filter_map(|(key, label, field)| {
let source = payload.get(field)?;
source.is_object().then(|| {
quota_window(
key,
label,
None,
vec![],
None,
json_number(source.get("utilization")),
json_iso(source.get("resets_at").or_else(|| source.get("resetsAt"))),
)
})
})
.collect()
}
fn jwt_account_id(account: &Account) -> Option<String> {
let token = string_at(&account.raw, &["/id_token", "/idToken"])?;
let payload = token.split('.').nth(1)?;
let bytes = URL_SAFE_NO_PAD.decode(payload).ok()?;
let value: Value = serde_json::from_slice(&bytes).ok()?;
string_at(
&value,
&[
"/chatgpt_account_id",
"/https:~1~1api.openai.com~1auth/chatgpt_account_id",
],
)
}
fn fetch_codex_usage(
client: &Client,
cpa: &CpaConfig,
account: &Account,
) -> Result<Vec<QuotaWindow>> {
let mut headers = json!({ "authorization": "Bearer $TOKEN$" });
if let Some(id) = jwt_account_id(account) {
headers["Chatgpt-Account-Id"] = Value::String(id);
}
let payload = api_call(client, cpa, account, "GET", CODEX_USAGE_URL, headers, None)?;
let windows = parse_codex_usage(&payload);
if windows.is_empty() {
bail!(ProviderError::new(
"codex",
"WHAM response contained no quota windows"
));
}
Ok(windows)
}
fn fetch_claude_usage(
client: &Client,
cpa: &CpaConfig,
account: &Account,
) -> Result<Vec<QuotaWindow>> {
let headers = json!({
"authorization": "Bearer $TOKEN$",
"anthropic-beta": "oauth-2025-04-20",
"content-type": "application/json"
});
let payload = api_call(client, cpa, account, "GET", CLAUDE_USAGE_URL, headers, None)?;
let windows = parse_claude_usage(&payload);
if windows.is_empty() {
bail!(ProviderError::new(
"claude",
"OAuth usage response contained no quota windows"
));
}
Ok(windows)
}
fn parse_antigravity_usage(payload: &Value) -> Vec<QuotaWindow> {
payload
.get("groups")
.and_then(Value::as_array)
.into_iter()
.flatten()
.flat_map(|group| {
let group_name = string_at(group, &["/displayName", "/display_name"])
.unwrap_or_else(|| "Antigravity Models".to_string());
let group_description = string_at(group, &["/description"]);
group
.get("buckets")
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter(|bucket| {
!bucket
.get("disabled")
.and_then(Value::as_bool)
.unwrap_or(false)
})
.map(move |bucket| {
let key = string_at(bucket, &["/bucketId", "/bucket_id"])
.unwrap_or_else(|| "quota".to_string());
let label = string_at(bucket, &["/displayName", "/display_name"])
.unwrap_or_else(|| key.clone());
let mut window = quota_window(
format!("{group_name}:{key}"),
label,
Some(group_name.clone()),
vec![],
json_number(
bucket
.get("remainingFraction")
.or_else(|| bucket.get("remaining_fraction")),
)
.map(|value| value * 100.0),
None,
json_iso(
bucket
.get("resetTime")
.or_else(|| bucket.get("reset_time"))
.or_else(|| bucket.get("resetsAt")),
),
);
window.group_description = group_description.clone();
window
})
})
.collect()
}
fn fetch_antigravity_usage(
client: &Client,
cpa: &CpaConfig,
account: &Account,
) -> Result<Vec<QuotaWindow>> {
let metadata = json!({ "ideType": "ANTIGRAVITY" });
let headers = json!({ "authorization": "Bearer $TOKEN$", "content-type": "application/json", "user-agent": "antigravity/1.11.5 darwin/arm64", "client-metadata": metadata.to_string() });
let assist = api_call(
client,
cpa,
account,
"POST",
GOOGLE_LOAD_ASSIST_URL,
headers.clone(),
Some(json!({ "metadata": metadata }).to_string()),
)?;
let project = string_at(
&account.raw,
&["/project_id", "/projectId", "/metadata/project_id"],
)
.or_else(|| string_at(&assist, &["/cloudaicompanionProject/id"]))
.or_else(|| {
assist
.get("cloudaicompanionProject")
.and_then(Value::as_str)
.map(ToString::to_string)
})
.ok_or_else(|| {
ProviderError::new(
"antigravity",
"CLIProxyAPI credential has no Code Assist project",
)
})?;
let payload = api_call(
client,
cpa,
account,
"POST",
ANTIGRAVITY_QUOTA_SUMMARY_URL,
headers,
Some(json!({ "project": project }).to_string()),
)?;
let windows = parse_antigravity_usage(&payload);
if windows.is_empty() {
bail!(ProviderError::new(
"antigravity",
"quota summary response contained no active quota buckets"
));
}
Ok(windows)
}
fn fetch_account(client: &Client, cpa: &CpaConfig, account: &Account) -> Result<AccountSnapshot> {
let windows = match account.provider.as_str() {
"codex" => fetch_codex_usage(client, cpa, account),
"claude" => fetch_claude_usage(client, cpa, account),
"antigravity" => fetch_antigravity_usage(client, cpa, account),
_ => bail!(ProviderError::new(
&account.provider,
"provider quota unavailable through CLIProxyAPI"
)),
}?;
Ok(AccountSnapshot {
provider: account.provider.clone(),
account_id: account.id.clone(),
account_label: account.label.clone(),
fetched_at: now(),
windows,
})
}
fn record_account_result(
report: &mut UsageReport,
account: &Account,
result: Result<AccountSnapshot>,
) {
match result {
Ok(snapshot) => report.accounts.push(snapshot),
Err(error) => {
let message = error.to_string();
report.errors.push(ReportError {
provider: account.provider.clone(),
account_id: account.id.clone(),
account_label: account.label.clone(),
message: message
.strip_prefix(&format!("{}: ", account.provider))
.unwrap_or(&message)
.to_string(),
});
}
}
}
fn collect_usage(
config: &CpaConfig,
provider: Option<&str>,
timeout_ms: u64,
) -> Result<UsageReport> {
if let Some(provider) = provider
&& !matches!(provider, "claude" | "codex" | "antigravity")
{
bail!("--provider must be claude, codex, or antigravity");
}
let client = client(timeout_ms)?;
let mut accounts =
normalize_accounts(management_request(&client, config, "/auth-files", None)?)?;
if let Some(provider) = provider {
accounts.retain(|account| account.provider == provider);
}
if accounts.is_empty() {
bail!("no accounts match requested provider");
}
let mut report = UsageReport {
version: FORMAT_VERSION,
generated_at: now(),
accounts: vec![],
errors: vec![],
};
for account in accounts {
let result = fetch_account(&client, config, &account);
record_account_result(&mut report, &account, result);
}
Ok(report)
}
fn format_percent(value: Option<f64>) -> String {
match value {
Some(value) if value.fract() == 0.0 => format!("{value:.0}%"),
Some(value) => format!("{value:.1}%"),
None => "unknown".to_string(),
}
}
fn format_usage(percent_used: Option<f64>, remaining_percent: Option<f64>) -> String {
if let Some(value) = percent_used {
format!("{} used", format_percent(Some(value)))
} else if let Some(value) = remaining_percent {
format!("provider reports {} remaining", format_percent(Some(value)))
} else {
"not reported".to_string()
}
}
fn format_reset(value: Option<&str>) -> String {
value
.and_then(|value| DateTime::parse_from_rfc3339(value).ok())
.map(|date| {
date.with_timezone(&Local)
.format("%Y-%m-%d %H:%M:%S %Z")
.to_string()
})
.unwrap_or_else(|| "unknown".to_string())
}
fn title(value: &str) -> String {
let mut chars = value.chars();
chars
.next()
.map(|first| first.to_uppercase().collect::<String>() + chars.as_str())
.unwrap_or_default()
}
fn human_report(report: &UsageReport) -> String {
let mut output = String::new();
for account in &report.accounts {
let label = if account
.account_label
.eq_ignore_ascii_case(&account.provider)
{
String::new()
} else {
format!(" ({})", account.account_label)
};
output.push_str(&format!("{}{}\n", title(&account.provider), label));
let mut previous_group = None;
for window in &account.windows {
if window.group != previous_group {
if let Some(group) = &window.group {
output.push_str(&format!(" {group}\n"));
}
previous_group = window.group.clone();
}
let indent = if window.group.is_some() { " " } else { " " };
let efforts = window
.efforts
.as_ref()
.filter(|value| !value.is_empty())
.map(|value| {
format!(
"; efforts {}",
value
.iter()
.map(|value| title(value))
.collect::<Vec<_>>()
.join(", ")
)
})
.unwrap_or_default();
output.push_str(&format!(
"{indent}{}: {}; resets {}{}\n",
window.label,
format_usage(window.percent_used, window.remaining_percent),
format_reset(window.resets_at.as_deref()),
efforts
));
}
if account.windows.is_empty() {
output.push_str(" No quota windows returned.\n");
}
}
for error in &report.errors {
output.push_str(&format!(
"{} ({}): {}\n",
title(&error.provider),
error.account_id,
error.message
));
}
output
}
fn write_report(path: &Path, report: &UsageReport) -> Result<()> {
write_secure(
path,
format!("{}\n", serde_json::to_string_pretty(report)?).as_bytes(),
)
}
fn run_poll(
config: CpaConfig,
interval: u64,
once: bool,
json: bool,
provider: Option<String>,
output: PathBuf,
timeout: u64,
) -> Result<()> {
let (shutdown_tx, shutdown_rx) = mpsc::channel();
ctrlc::set_handler(move || {
let _ = shutdown_tx.send(());
})
.context("install signal handler")?;
loop {
let report = collect_usage(&config, provider.as_deref(), timeout)?;
write_report(&output, &report)?;
if json {
println!("{}", serde_json::to_string(&report)?);
} else {
println!("Wrote {}", output.display());
}
if once {
return Ok(());
}
if shutdown_rx
.recv_timeout(Duration::from_secs(interval))
.is_ok()
{
return Ok(());
}
}
}
fn command_exists(command: &str) -> bool {
ProcessCommand::new(command)
.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.is_ok_and(|status| status.success())
}
fn cpa_asset_name(version: &str) -> Result<String> {
let os = match env::consts::OS {
"macos" => "darwin",
"windows" => "windows",
other => other,
};
let arch = match env::consts::ARCH {
"aarch64" => "aarch64",
"x86_64" => "amd64",
other => bail!("CLIProxyAPI download unsupported architecture: {other}"),
};
Ok(format!(
"CLIProxyAPI_{version}_{os}_{arch}.{}",
if os == "windows" { "zip" } else { "tar.gz" }
))
}
fn github_json(client: &Client, url: &str) -> Result<Value> {
request_json(
client,
"cliproxyapi",
client
.get(url)
.header("accept", "application/vnd.github+json"),
)
}
fn download_response(client: &Client, url: &str) -> Result<Response> {
client
.get(url)
.send()
.with_context(|| format!("download {url}"))?
.error_for_status()
.context("CLIProxyAPI download failed")
}
fn checksum(asset: &str, text: &str) -> Option<String> {
text.lines().find_map(|line| {
let mut parts = line.split_whitespace();
let hash = parts.next()?;
let name = parts.next()?.trim_start_matches('*');
(name == asset && hash.len() == 64).then(|| hash.to_lowercase())
})
}
fn download_cpa<F: FnMut(DownloadProgress)>(client: &Client, mut progress: F) -> Result<PathBuf> {
progress(DownloadProgress {
downloaded: 0,
total: None,
stage: "Checking CLIProxyAPI release",
});
let release = github_json(client, RELEASE_URL)?;
let version = release
.get("tag_name")
.and_then(Value::as_str)
.unwrap_or_default()
.trim_start_matches('v');
if version.is_empty() {
bail!("CLIProxyAPI release response missing version");
}
let asset_name = cpa_asset_name(version)?;
let assets = release
.get("assets")
.and_then(Value::as_array)
.ok_or_else(|| anyhow!("CLIProxyAPI release lacks assets"))?;
let asset_url = assets
.iter()
.find(|asset| asset.get("name").and_then(Value::as_str) == Some(&asset_name))
.and_then(|asset| asset.get("browser_download_url"))
.and_then(Value::as_str)
.ok_or_else(|| anyhow!("CLIProxyAPI release lacks {asset_name}"))?;
let checksum_url = assets
.iter()
.find(|asset| asset.get("name").and_then(Value::as_str) == Some("checksums.txt"))
.and_then(|asset| asset.get("browser_download_url"))
.and_then(Value::as_str)
.ok_or_else(|| anyhow!("CLIProxyAPI release lacks checksums.txt"))?;
let checksum_text = download_response(client, checksum_url)?.text()?;
let expected = checksum(&asset_name, &checksum_text)
.ok_or_else(|| anyhow!("CLIProxyAPI checksum missing for {asset_name}"))?;
let directory = qota_dir()?.join("cpa").join(version);
secure_directory(&directory)?;
let archive = directory.join(&asset_name);
let mut response = download_response(client, asset_url)?;
let total = response.content_length();
let mut file = File::create(&archive)?;
let mut hasher = Sha256::new();
let mut downloaded = 0u64;
let mut buffer = [0_u8; 64 * 1024];
progress(DownloadProgress {
downloaded,
total,
stage: "Downloading CLIProxyAPI",
});
loop {
let read = response.read(&mut buffer)?;
if read == 0 {
break;
}
file.write_all(&buffer[..read])?;
hasher.update(&buffer[..read]);
downloaded += read as u64;
progress(DownloadProgress {
downloaded,
total,
stage: "Downloading CLIProxyAPI",
});
}
file.sync_all()?;
progress(DownloadProgress {
downloaded,
total,
stage: "Verifying SHA-256",
});
let actual = format!("{:x}", hasher.finalize());
if actual != expected {
bail!("CLIProxyAPI checksum mismatch for {asset_name}");
}
progress(DownloadProgress {
downloaded,
total,
stage: "Extracting CLIProxyAPI",
});
let success = if env::consts::OS == "windows" {
ProcessCommand::new("powershell")
.args([
"-NoProfile",
"-Command",
&format!(
"Expand-Archive -LiteralPath '{}' -DestinationPath '{}' -Force",
archive.display(),
directory.display()
),
])
.status()?
.success()
} else {
ProcessCommand::new("tar")
.args([
"-xzf",
archive.to_string_lossy().as_ref(),
"-C",
directory.to_string_lossy().as_ref(),
])
.status()?
.success()
};
if !success {
bail!("CLIProxyAPI archive extraction failed");
}
let binary = find_binary(&directory)?
.ok_or_else(|| anyhow!("CLIProxyAPI archive contained no executable"))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&binary, fs::Permissions::from_mode(0o700))?;
}
Ok(binary)
}
fn find_binary(directory: &Path) -> Result<Option<PathBuf>> {
for entry in fs::read_dir(directory)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
if let Some(path) = find_binary(&path)? {
return Ok(Some(path));
}
} else if matches!(
path.file_name().and_then(|name| name.to_str()),
Some("CLIProxyAPI")
| Some("cli-proxy-api")
| Some("CLIProxyAPI.exe")
| Some("cli-proxy-api.exe")
) {
return Ok(Some(path));
}
}
Ok(None)
}
fn random_key() -> Result<String> {
let mut bytes = [0_u8; 32];
getrandom::fill(&mut bytes).map_err(|error| anyhow!("generate local secret: {error}"))?;
Ok(bytes.iter().map(|byte| format!("{byte:02x}")).collect())
}
fn sidecar_running() -> bool {
TcpStream::connect_timeout(
&"127.0.0.1:8317".parse().unwrap(),
Duration::from_millis(250),
)
.is_ok()
}
fn write_cpa_files(path: &Path, existing: Option<&CpaConfig>, binary: &Path) -> Result<CpaConfig> {
let directory = path
.parent()
.ok_or_else(|| anyhow!("config path has no parent"))?;
secure_directory(directory)?;
let auth = directory.join("auth");
secure_directory(&auth)?;
let key = existing
.map(|config| config.management_key.clone())
.unwrap_or(random_key()?);
let cpa_path = directory.join("cliproxyapi.yaml");
let yaml = format!(
"host: \"127.0.0.1\"\nport: 8317\nremote-management:\n allow-remote: false\n secret-key: \"{key}\"\n disable-control-panel: true\nauth-dir: \"{}\"\napi-keys: []\n",
auth.display()
);
write_secure(&cpa_path, yaml.as_bytes())?;
let refresh_interval = existing.map(|c| c.refresh_interval).unwrap_or(60);
let stored = StoredConfig {
cpa_url: Some(DEFAULT_CPA_URL.to_string()),
management_key: key.clone(),
cpa_binary: Some(binary.display().to_string()),
cpa_config_path: Some(cpa_path.display().to_string()),
refresh_interval: Some(refresh_interval),
};
write_secure(
path,
format!("{}\n", serde_json::to_string_pretty(&stored)?).as_bytes(),
)?;
Ok(CpaConfig {
url: DEFAULT_CPA_URL.to_string(),
management_key: key,
binary: Some(binary.to_path_buf()),
cpa_config_path: Some(cpa_path),
refresh_interval,
})
}
fn forward_oauth_lines<R: Read + Send + 'static>(stream: R, sender: mpsc::Sender<String>) {
thread::spawn(move || {
for line in BufReader::new(stream).lines().map_while(|line| line.ok()) {
let _ = sender.send(line);
}
});
}
fn spawn_login(
binary: &Path,
cpa_path: &Path,
provider: &str,
) -> Result<(Child, ChildStdin, mpsc::Receiver<String>)> {
let mut child = ProcessCommand::new(binary)
.args([
"-config",
cpa_path.to_string_lossy().as_ref(),
&format!("-{provider}-login"),
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.with_context(|| format!("start {provider} OAuth"))?;
let stdin = child
.stdin
.take()
.ok_or_else(|| anyhow!("{provider} OAuth stdin unavailable"))?;
let (sender, receiver) = mpsc::channel();
if let Some(stdout) = child.stdout.take() {
forward_oauth_lines(stdout, sender.clone());
}
if let Some(stderr) = child.stderr.take() {
forward_oauth_lines(stderr, sender.clone());
}
Ok((child, stdin, receiver))
}
fn safe_oauth_line(value: &str) -> String {
let lowered = value.to_lowercase();
if [
"token",
"authorization",
"secret",
"cookie",
"api-key",
"api_key",
]
.iter()
.any(|needle| lowered.contains(needle))
{
"Sensitive provider output hidden".to_string()
} else {
value.chars().take(160).collect()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum OAuthPhase {
Starting,
Browser,
Callback,
Prompt,
}
fn oauth_phase(logs: &[String], has_input: bool) -> OAuthPhase {
if has_input
|| logs.iter().any(|line| {
let line = line.to_lowercase();
line.contains("enter ") || line.contains("type ") || line.contains("prompt")
})
{
return OAuthPhase::Prompt;
}
if logs.iter().any(|line| {
let line = line.to_lowercase();
line.contains("callback")
|| line.contains("redirect received")
|| line.contains("authorization complete")
}) {
return OAuthPhase::Callback;
}
if logs.iter().any(|line| {
let line = line.to_lowercase();
line.contains("browser") || line.contains("open http") || line.contains("visit http")
}) {
return OAuthPhase::Browser;
}
OAuthPhase::Starting
}
fn oauth_message(phase: OAuthPhase) -> &'static str {
match phase {
OAuthPhase::Starting => "Starting local OAuth service…",
OAuthPhase::Browser => "Browser login opened. Approve the provider account.",
OAuthPhase::Callback => "Approval received. Waiting for local callback to finish.",
OAuthPhase::Prompt => "Provider requested a reply. Type it below, then press Enter.",
}
}
fn oauth_log_style(line: &str) -> Style {
let lowered = line.to_lowercase();
if lowered.contains("hidden") || lowered.contains("error") || lowered.contains("failed") {
Style::default().fg(color(Color::Red))
} else if lowered.contains("browser") || lowered.contains("http") {
Style::default().fg(color(Color::Cyan))
} else if lowered.contains("complete") || lowered.contains("success") {
Style::default().fg(color(Color::Green))
} else {
Style::default().fg(color(Color::DarkGray))
}
}
fn start_cpa(binary: &Path, cpa_path: &Path) -> Result<u32> {
let child = ProcessCommand::new(binary)
.args(["-config", cpa_path.to_string_lossy().as_ref()])
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
Ok(child.id())
}
#[derive(Clone, Default)]
struct SetupSelection {
claude: bool,
codex: bool,
antigravity: bool,
cursor: usize,
}
impl SetupSelection {
fn providers(&self) -> Vec<&'static str> {
[
self.claude.then_some("claude"),
self.codex.then_some("codex"),
self.antigravity.then_some("antigravity"),
]
.into_iter()
.flatten()
.collect()
}
fn move_cursor(&mut self, forward: bool) {
self.cursor = if forward {
(self.cursor + 1).min(2)
} else {
self.cursor.saturating_sub(1)
};
}
fn toggle_cursor(&mut self) {
match self.cursor {
0 => self.claude = !self.claude,
1 => self.codex = !self.codex,
_ => self.antigravity = !self.antigravity,
}
}
}
const SETUP_STEPS: [&str; 5] = [
"Choose provider",
"Install backend",
"Sign in",
"Verify quota",
"Done",
];
fn setup_stepper(active: usize) -> Line<'static> {
let mut spans = vec![];
for (index, step) in SETUP_STEPS.iter().enumerate() {
let (glyph, style) = if index < active {
("\u{2713}", Style::default().fg(c_ok()))
} else if index == active {
(
"\u{25b8}",
Style::default().fg(c_info()).add_modifier(Modifier::BOLD),
)
} else {
("\u{b7}", Style::default().fg(c_meta()))
};
spans.push(Span::styled(format!("{glyph} "), style));
spans.push(Span::styled(
(*step).to_string(),
if index == active {
Style::default().fg(c_bright()).add_modifier(Modifier::BOLD)
} else if index < active {
Style::default().fg(c_ok())
} else {
Style::default().fg(c_meta())
},
));
if index < SETUP_STEPS.len() - 1 {
spans.push(Span::styled(" \u{203a} ", Style::default().fg(c_track())));
}
}
Line::from(spans)
}
fn setup_chrome(frame: &mut Frame, subtitle: &str, active: usize, hints: &[(&str, &str)]) -> Rect {
let area = frame.area();
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
Constraint::Length(1),
])
.split(area);
frame.render_widget(
Paragraph::new(Line::from(vec![
Span::styled(
"\u{25aa} QOTA",
Style::default().fg(c_accent()).add_modifier(Modifier::BOLD),
),
Span::raw(" "),
Span::styled(subtitle.to_string(), Style::default().fg(c_faint())),
])),
rows[0],
);
frame.render_widget(Paragraph::new(setup_stepper(active)), rows[1]);
draw_hairline(frame, rows[2]);
let mut hint_spans = vec![];
for (key, label) in hints {
hint_spans.push(Span::styled(
*key,
Style::default().fg(c_accent()).add_modifier(Modifier::BOLD),
));
hint_spans.push(Span::styled(
format!(" {label} "),
Style::default().fg(c_meta()),
));
}
frame.render_widget(Paragraph::new(Line::from(hint_spans)), rows[4]);
rows[3]
}
fn draw_setup_select(frame: &mut Frame, selection: &SetupSelection) {
let area = setup_chrome(
frame,
"Setup \u{b7} choose provider",
0,
&[
("\u{2190}\u{2191}\u{2193}\u{2192}", "Select"),
("Click", "Toggle"),
("Space", "Toggle"),
("Enter", "Continue"),
("Esc", "Cancel"),
],
);
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Length(2),
Constraint::Length(12),
Constraint::Min(0),
])
.split(area);
frame.render_widget(
Paragraph::new(Line::from(Span::styled(
"Choose a provider",
Style::default().fg(c_bright()).add_modifier(Modifier::BOLD),
))),
rows[0],
);
frame.render_widget(
Paragraph::new(Line::from(Span::styled(
"Select with arrow keys or click, Space to toggle, Enter to continue. Nothing is downloaded and no browser opens until you confirm.",
Style::default().fg(c_meta()),
)))
.wrap(Wrap { trim: false }),
rows[1],
);
let providers = [
(
"Claude",
selection.claude,
"Anthropic Claude Code quota windows",
"Local backend \u{b7} OAuth sign-in",
),
(
"Codex",
selection.codex,
"OpenAI Codex usage via local backend",
"Local CLI backend \u{b7} OAuth sign-in",
),
(
"Antigravity",
selection.antigravity,
"Google Antigravity model quotas",
"Local CLI backend \u{b7} OAuth sign-in",
),
];
let cards = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(4),
Constraint::Length(4),
Constraint::Length(4),
])
.split(rows[2]);
for (index, (name, checked, description, meta)) in providers.into_iter().enumerate() {
let focused = selection.cursor == index;
let block = Block::default().borders(Borders::ALL).border_style(
Style::default()
.fg(if focused { c_accent() } else { c_hair() })
.add_modifier(if focused {
Modifier::BOLD
} else {
Modifier::empty()
}),
);
let inner = block.inner(cards[index]);
frame.render_widget(block, cards[index]);
let lines = vec![
Line::from(vec![
Span::styled(
if focused { "\u{25b6}\u{25b6} " } else { " " },
Style::default()
.fg(if focused { c_accent() } else { c_hair() })
.add_modifier(if focused {
Modifier::BOLD
} else {
Modifier::empty()
}),
),
Span::styled(
if checked { "[x] " } else { "[ ] " },
Style::default().fg(if checked { c_accent() } else { c_meta() }),
),
badge(name),
]),
Line::from(Span::styled(
format!(" {description} \u{b7} {meta}"),
Style::default().fg(c_faint()),
)),
];
frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner);
}
}
fn setup_provider_at(area: Rect, column: u16, row: u16) -> Option<usize> {
let content_y = area.y.saturating_add(3);
let cards_y = content_y.saturating_add(3);
if column < area.x || column >= area.right() || row < cards_y {
return None;
}
let index = ((row - cards_y) / 4) as usize;
(index < 3 && row < cards_y.saturating_add(12)).then_some(index)
}
fn handle_setup_mouse(selection: &mut SetupSelection, mouse: MouseEvent, area: Rect) {
if !matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) {
return;
}
if let Some(index) = setup_provider_at(area, mouse.column, mouse.row) {
selection.cursor = index;
selection.toggle_cursor();
}
}
fn centered_rect(width: u16, height: u16, area: ratatui::layout::Rect) -> ratatui::layout::Rect {
let horizontal = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - width) / 2),
Constraint::Percentage(width),
Constraint::Percentage((100 - width) / 2),
])
.split(area);
Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - height) / 2),
Constraint::Percentage(height),
Constraint::Percentage((100 - height) / 2),
])
.split(horizontal[1])[1]
}
fn draw_setup_progress<B: Backend>(
terminal: &mut Terminal<B>,
progress: DownloadProgress,
) -> Result<()> {
terminal.draw(|frame| draw_setup_download(frame, progress))?;
Ok(())
}
const DOWNLOAD_STAGES: [&str; 4] = [
"Checking CLIProxyAPI release",
"Downloading CLIProxyAPI",
"Verifying SHA-256",
"Extracting CLIProxyAPI",
];
fn draw_setup_download(frame: &mut Frame, progress: DownloadProgress) {
let area = setup_chrome(
frame,
"Setup \u{b7} install local backend",
1,
&[("Esc", "Cancel")],
);
let ratio = progress
.total
.map(|total| {
if total == 0 {
0.0
} else {
progress.downloaded as f64 / total as f64
}
})
.unwrap_or(0.0);
let active = DOWNLOAD_STAGES
.iter()
.position(|stage| *stage == progress.stage)
.unwrap_or(0);
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(4),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
])
.split(area);
frame.render_widget(
Paragraph::new(Line::from(Span::styled(
"Installing local backend",
Style::default().fg(c_bright()).add_modifier(Modifier::BOLD),
))),
rows[0],
);
let stage_lines: Vec<Line> = DOWNLOAD_STAGES
.iter()
.enumerate()
.map(|(index, stage)| {
let (glyph, style) = if index < active {
("\u{2713}", Style::default().fg(c_ok()))
} else if index == active {
(
"\u{25b8}",
Style::default().fg(c_info()).add_modifier(Modifier::BOLD),
)
} else {
("\u{b7}", Style::default().fg(c_meta()))
};
Line::from(vec![
Span::styled(format!("{glyph} "), style),
Span::styled((*stage).to_string(), style),
])
})
.collect();
frame.render_widget(Paragraph::new(stage_lines), rows[2]);
frame.render_widget(
Paragraph::new(Line::from(text_bar(
ratio,
rows[3].width as usize,
c_info(),
))),
rows[3],
);
let detail = match progress.total {
Some(total) => format!(
"{:.0}% {} / {}",
ratio.clamp(0.0, 1.0) * 100.0,
bytes(progress.downloaded),
bytes(total)
),
None => bytes(progress.downloaded),
};
frame.render_widget(
Paragraph::new(Line::from(Span::styled(
detail,
Style::default().fg(c_meta()),
))),
rows[4],
);
}
fn bytes(value: u64) -> String {
if value >= 1024 * 1024 {
format!("{:.1} MiB", value as f64 / (1024.0 * 1024.0))
} else if value >= 1024 {
format!("{:.1} KiB", value as f64 / 1024.0)
} else {
format!("{value} B")
}
}
fn select_providers_tui(terminal: &mut AppTerminal) -> Result<Option<SetupSelection>> {
let mut selection = SetupSelection::default();
loop {
terminal.draw(|frame| draw_setup_select(frame, &selection))?;
match event::read()? {
Event::Key(key) => {
if key.kind != KeyEventKind::Press {
continue;
}
match key.code {
KeyCode::Up | KeyCode::Left | KeyCode::Char('k') | KeyCode::Char('h') => {
selection.move_cursor(false)
}
KeyCode::Down | KeyCode::Right | KeyCode::Char('j') | KeyCode::Char('l') => {
selection.move_cursor(true)
}
KeyCode::Char(' ') => selection.toggle_cursor(),
KeyCode::Enter => {
if selection.providers().is_empty() {
continue;
}
return Ok(Some(selection));
}
KeyCode::Esc | KeyCode::Char('q') => return Ok(None),
_ => {}
}
}
Event::Mouse(mouse) => {
let size = terminal.size()?;
handle_setup_mouse(
&mut selection,
mouse,
Rect::new(0, 0, size.width, size.height),
)
}
_ => {}
}
}
}
fn oauth_step(label: &str, position: usize, active: usize) -> Line<'static> {
let (icon, style) = if position < active {
("\u{2713}", Style::default().fg(c_ok()))
} else if position == active {
(
"\u{25d0}",
Style::default().fg(c_info()).add_modifier(Modifier::BOLD),
)
} else {
("\u{b7}", Style::default().fg(c_meta()))
};
Line::from(vec![
Span::styled(format!("{icon} "), style),
Span::styled(label.to_string(), style),
])
}
fn draw_oauth_tui(frame: &mut Frame, provider: &str, logs: &[String], input: &str) {
let phase = oauth_phase(logs, !input.is_empty());
let active = match phase {
OAuthPhase::Starting => 0,
OAuthPhase::Browser | OAuthPhase::Prompt => 2,
OAuthPhase::Callback => 3,
};
let area = setup_chrome(
frame,
&format!("Setup \u{b7} {}", title(provider)),
2,
&[("Esc", "Cancel safely")],
);
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(2),
Constraint::Length(4),
Constraint::Min(4),
Constraint::Length(4),
])
.split(area);
frame.render_widget(
Paragraph::new(Text::from(vec![
Line::from(Span::styled(
format!("Signing in to {}", title(provider)),
Style::default().fg(c_bright()).add_modifier(Modifier::BOLD),
)),
Line::from(Span::styled(
oauth_message(phase),
Style::default().fg(c_info()),
)),
])),
chunks[0],
);
frame.render_widget(
Paragraph::new(Text::from(vec![
oauth_step("Start local authentication", 0, active),
oauth_step("Open browser login", 1, active),
oauth_step("Approve provider account", 2, active),
oauth_step("Receive local callback", 3, active),
])),
chunks[1],
);
let output = if logs.is_empty() {
vec![Line::from(Span::styled(
"Waiting for CLIProxyAPI\u{2026}",
Style::default().fg(c_meta()),
))]
} else {
logs.iter()
.map(|line| {
Line::from(vec![
Span::styled("\u{203a} ", Style::default().fg(c_accent())),
Span::styled(line.clone(), oauth_log_style(line)),
])
})
.collect()
};
frame.render_widget(
Paragraph::new(Text::from(output))
.block(
Block::default()
.borders(Borders::TOP)
.border_style(Style::default().fg(c_hair()))
.title(Span::styled(
" Provider status ",
Style::default().fg(c_meta()),
)),
)
.wrap(Wrap { trim: false }),
chunks[2],
);
let masked = "\u{2022}".repeat(input.chars().count());
frame.render_widget(
Paragraph::new(Text::from(vec![
Line::from(vec![
Span::styled("\u{203a} ", Style::default().fg(c_accent())),
Span::styled(masked, Style::default().fg(c_text())),
]),
Line::from(Span::styled(
"Type reply for a provider prompt; Enter sends. Esc cancels. Input stays masked.",
Style::default().fg(c_faint()),
)),
]))
.block(
Block::default()
.borders(Borders::TOP)
.border_style(Style::default().fg(c_hair()))
.title(Span::styled(" Typed input ", Style::default().fg(c_meta()))),
),
chunks[3],
);
}
fn countdown_seconds(deadline: Instant) -> u64 {
let remaining = deadline.saturating_duration_since(Instant::now());
if remaining.is_zero() {
0
} else {
remaining.as_secs().saturating_add(1)
}
}
fn draw_dashboard_return_tui(frame: &mut Frame, message: &str, seconds: u64) {
let area = setup_chrome(
frame,
"Setup \u{b7} complete",
4,
&[("Enter", "Return now"), ("Esc", "Return now")],
);
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Length(2),
Constraint::Length(1),
Constraint::Min(0),
])
.split(area);
frame.render_widget(
Paragraph::new(Line::from(Span::styled(
"✓ PROVIDERS CONNECTED",
Style::default().fg(c_ok()).add_modifier(Modifier::BOLD),
))),
rows[0],
);
frame.render_widget(
Paragraph::new(Line::from(Span::styled(
message.to_string(),
Style::default().fg(c_meta()),
)))
.wrap(Wrap { trim: false }),
rows[1],
);
frame.render_widget(
Paragraph::new(Line::from(vec![
Span::styled(
format!("Returning to dashboard in {seconds}s"),
Style::default().fg(c_bright()).add_modifier(Modifier::BOLD),
),
Span::styled(
" Live quota will load there.",
Style::default().fg(c_faint()),
),
])),
rows[2],
);
}
fn show_dashboard_return_countdown(terminal: &mut AppTerminal, message: &str) -> Result<()> {
let deadline = Instant::now() + Duration::from_secs(5);
loop {
let seconds = countdown_seconds(deadline);
terminal.draw(|frame| draw_dashboard_return_tui(frame, message, seconds))?;
if seconds == 0 {
return Ok(());
}
if !event::poll(Duration::from_millis(100))? {
continue;
}
match event::read()? {
Event::Key(key)
if key.kind == KeyEventKind::Press
&& matches!(key.code, KeyCode::Enter | KeyCode::Esc | KeyCode::Char('q')) =>
{
return Ok(());
}
Event::Mouse(mouse)
if matches!(mouse.kind, MouseEventKind::Down(MouseButton::Left)) =>
{
return Ok(());
}
_ => {}
}
}
}
fn run_login_tui(
terminal: &mut AppTerminal,
binary: &Path,
cpa_path: &Path,
provider: &str,
) -> Result<()> {
let (mut child, mut stdin, receiver) = spawn_login(binary, cpa_path, provider)?;
let mut logs = vec![format!("Launching {provider} OAuth…")];
let mut input = String::new();
loop {
while let Ok(line) = receiver.try_recv() {
logs.push(safe_oauth_line(&line));
if logs.len() > 8 {
logs.remove(0);
}
}
terminal.draw(|frame| draw_oauth_tui(frame, provider, &logs, &input))?;
if let Some(exit) = child.try_wait()? {
if exit.success() {
return Ok(());
}
bail!(
"{provider} OAuth exited {}",
exit.code()
.map_or("without exit code".to_string(), |code| code.to_string())
);
}
if !event::poll(Duration::from_millis(125))? {
continue;
}
if let Event::Key(key) = event::read()? {
if key.kind != KeyEventKind::Press {
continue;
}
match key.code {
KeyCode::Char(character) => input.push(character),
KeyCode::Backspace => {
input.pop();
}
KeyCode::Enter => {
if !input.is_empty() {
stdin.write_all(input.as_bytes())?;
stdin.write_all(b"\n")?;
stdin.flush()?;
logs.push("Typed reply sent to CLIProxyAPI.".to_string());
input.clear();
}
}
KeyCode::Esc => {
let _ = child.kill();
bail!("{provider} OAuth cancelled");
}
_ => {}
}
}
}
}
fn selection_from_providers(providers: Vec<String>) -> SetupSelection {
SetupSelection {
claude: providers.iter().any(|value| value == "claude"),
codex: providers.iter().any(|value| value == "codex"),
antigravity: providers.iter().any(|value| value == "antigravity"),
cursor: 0,
}
}
fn prepare_setup<F: FnMut(DownloadProgress)>(config_path: &Path, progress: F) -> Result<CpaConfig> {
let existing = config_path
.exists()
.then(|| read_cpa_config(config_path))
.transpose()?;
let cpa_candidate = env::var_os("QOTA_CPA_BIN")
.map(PathBuf::from)
.or_else(|| existing.as_ref().and_then(|value| value.binary.clone()))
.filter(|path| path.exists());
let binary = if let Some(binary) = cpa_candidate {
binary
} else if command_exists("cli-proxy-api") {
PathBuf::from("cli-proxy-api")
} else if command_exists("cliproxy") {
PathBuf::from("cliproxy")
} else {
download_cpa(&client(60_000)?, progress)?
};
write_cpa_files(config_path, existing.as_ref(), &binary)
}
fn cpa_paths(cpa: &CpaConfig) -> Result<(&Path, &Path)> {
Ok((
cpa.binary
.as_deref()
.ok_or_else(|| anyhow!("CLIProxyAPI binary missing"))?,
cpa.cpa_config_path
.as_deref()
.ok_or_else(|| anyhow!("CPA config path missing"))?,
))
}
fn finish_setup(cpa: &CpaConfig) -> Result<String> {
let (binary, cpa_path) = cpa_paths(cpa)?;
if sidecar_running() {
Ok("CLIProxyAPI already running. Qota is ready.".to_string())
} else {
Ok(format!(
"CLIProxyAPI started ({}). Qota is ready.",
start_cpa(binary, cpa_path)?
))
}
}
fn run_login_plain(binary: &Path, cpa_path: &Path, provider: &str) -> Result<()> {
let status = ProcessCommand::new(binary)
.args([
"-config",
cpa_path.to_string_lossy().as_ref(),
&format!("-{provider}-login"),
])
.status()?;
if status.success() {
Ok(())
} else {
bail!(
"{provider} OAuth exited {}",
status
.code()
.map_or("without exit code".to_string(), |code| code.to_string())
)
}
}
fn setup_tui(
terminal: &mut AppTerminal,
config_path: &Path,
forced: Option<Vec<String>>,
) -> Result<Option<String>> {
let selection = match forced {
Some(providers) => selection_from_providers(providers),
None => match select_providers_tui(terminal)? {
Some(selection) => selection,
None => return Ok(None),
},
};
let cpa = prepare_setup(config_path, |progress| {
draw_setup_progress(terminal, progress).unwrap_or(())
})?;
let cpa_path = cpa
.cpa_config_path
.as_ref()
.ok_or_else(|| anyhow!("CPA config path missing"))?;
let binary = cpa
.binary
.as_ref()
.ok_or_else(|| anyhow!("CLIProxyAPI binary missing"))?;
for provider in selection.providers() {
run_login_tui(terminal, binary, cpa_path, provider)?;
}
Ok(Some(finish_setup(&cpa)?))
}
fn setup_interactive(config_path: PathBuf, forced: Option<Vec<String>>) -> Result<()> {
if !io::stdout().is_terminal() {
let Some(providers) = forced else {
bail!("qota setup needs an interactive terminal; use --providers for automation");
};
let selection = selection_from_providers(providers);
let mut previous_stage = "";
let cpa = prepare_setup(&config_path, |progress| {
if progress.stage != previous_stage {
eprintln!("{}", progress.stage);
previous_stage = progress.stage;
}
})?;
let (binary, cpa_path) = cpa_paths(&cpa)?;
for provider in selection.providers() {
run_login_plain(binary, cpa_path, provider)?;
}
println!("{}", finish_setup(&cpa)?);
return Ok(());
}
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
let result = setup_tui(&mut terminal, &config_path, forced);
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
DisableMouseCapture,
LeaveAlternateScreen
)?;
terminal.show_cursor()?;
if let Some(message) = result? {
println!("{message}");
}
Ok(())
}
fn rgb(r: u8, g: u8, b: u8) -> Color {
color(Color::Rgb(r, g, b))
}
fn c_accent() -> Color {
rgb(0x4e, 0xc9, 0xb8)
}
fn c_ok() -> Color {
rgb(0x4e, 0xc9, 0xb8)
}
fn c_warn() -> Color {
rgb(0xe0, 0xa5, 0x4a)
}
fn c_err() -> Color {
rgb(0xe2, 0x66, 0x5e)
}
fn c_info() -> Color {
rgb(0x56, 0xc2, 0xd6)
}
fn c_meta() -> Color {
rgb(0x7d, 0x85, 0x8b)
}
fn c_text() -> Color {
rgb(0xdf, 0xe4, 0xe8)
}
fn c_bright() -> Color {
rgb(0xee, 0xf2, 0xf4)
}
fn c_dim() -> Color {
rgb(0xc3, 0xca, 0xcf)
}
fn c_faint() -> Color {
rgb(0x6b, 0x72, 0x78)
}
fn c_track() -> Color {
rgb(0x23, 0x2a, 0x30)
}
fn c_sel_bg() -> Color {
rgb(0x18, 0x20, 0x26)
}
fn c_hair() -> Color {
rgb(0x23, 0x2a, 0x30)
}
fn c_focus() -> Color {
rgb(0x2f, 0x6f, 0x6a)
}
fn draw_hairline(frame: &mut Frame, area: Rect) {
if area.width == 0 || area.height == 0 {
return;
}
frame.render_widget(
Paragraph::new(Line::from(Span::styled(
"─".repeat(area.width as usize),
Style::default().fg(c_hair()),
))),
area,
);
}
#[derive(Clone, Copy, PartialEq)]
enum Health {
Live,
Partial,
Error,
}
fn health_color(health: Health) -> Color {
match health {
Health::Live => c_ok(),
Health::Partial => c_warn(),
Health::Error => c_err(),
}
}
fn health_sym(health: Health) -> &'static str {
match health {
Health::Live => "\u{25cf}", Health::Partial => "\u{25d0}", Health::Error => "\u{25b2}", }
}
fn status_text(health: Health) -> &'static str {
match health {
Health::Live => "Live data",
Health::Partial => "Partial provider data",
Health::Error => "Last fetch failed",
}
}
fn snapshot_health(snapshot: &AccountSnapshot) -> Health {
if snapshot
.windows
.iter()
.any(|window| window.percent_used.is_some() || window.remaining_percent.is_some())
{
Health::Live
} else {
Health::Partial
}
}
fn threshold_color(used: f64) -> Color {
if used >= 85.0 {
c_err()
} else if used >= 60.0 {
c_warn()
} else {
c_ok()
}
}
fn band_label(used: f64) -> &'static str {
if used >= 85.0 {
"critical"
} else if used >= 60.0 {
"warning"
} else {
"healthy"
}
}
fn fmt_pct(value: f64) -> String {
if value.fract() == 0.0 {
format!("{value:.0}%")
} else {
format!("{value:.1}%")
}
}
fn window_state_text(window: &QuotaWindow) -> String {
if let Some(used) = window.percent_used {
format!(
"{} used \u{b7} {} remaining",
fmt_pct(used),
fmt_pct(100.0 - used)
)
} else if let Some(remaining) = window.remaining_percent {
format!("Provider reports {} remaining", fmt_pct(remaining))
} else {
"Usage not reported by provider".to_string()
}
}
fn reset_label(value: Option<&str>) -> Option<String> {
let parsed = DateTime::parse_from_rfc3339(value?)
.ok()?
.with_timezone(&Local);
let today = Local::now().date_naive();
let day = if parsed.date_naive() == today {
"today".to_string()
} else {
parsed.format("%a").to_string()
};
Some(format!("Resets {}, {}", day, parsed.format("%-I:%M %p")))
}
fn codex_explanation(key: &str) -> &'static str {
match key {
"5h" => "Usage in the current 5-hour rolling window.",
"7d" => "Usage across the trailing 7-day window.",
_ => "Rolling usage window.",
}
}
fn codex_window_title(key: &str, label: &str) -> String {
match key {
"5h" => "5 hour rolling window".to_string(),
"7d" => "7 day rolling window".to_string(),
_ => label.to_string(),
}
}
fn text_bar(ratio: f64, width: usize, fill: Color) -> Vec<Span<'static>> {
let width = width.max(1);
let filled = (ratio.clamp(0.0, 1.0) * width as f64).round() as usize;
let mut spans = Vec::new();
if filled > 0 {
spans.push(Span::styled(
"\u{2588}".repeat(filled),
Style::default().fg(fill),
));
}
if filled < width {
spans.push(Span::styled(
"\u{2591}".repeat(width - filled),
Style::default().fg(c_track()),
));
}
spans
}
fn badge(text: &str) -> Span<'static> {
Span::styled(
format!(" {} ", text.to_uppercase()),
Style::default()
.fg(c_accent())
.bg(c_sel_bg())
.add_modifier(Modifier::BOLD),
)
}
#[derive(Clone)]
struct NavEntry {
provider: String,
email: String,
health: Health,
snapshot: Option<usize>,
error: Option<usize>,
}
#[derive(Clone, Copy, PartialEq)]
enum Focus {
Rail,
Main,
Status,
}
#[derive(Clone, Copy, PartialEq)]
enum LayoutMode {
Wide,
Medium,
Narrow,
}
struct App {
report: Option<UsageReport>,
entries: Vec<NavEntry>,
selected: usize,
focus: Focus,
help: bool,
details: bool,
status: String,
last_refreshed: String,
refreshing: bool,
refresh_failed: bool,
loading_frame: usize,
flash: bool,
scroll: u16,
}
impl App {
fn new(report: Option<UsageReport>, status: String) -> Self {
let mut app = App {
report: None,
entries: vec![],
selected: 0,
focus: Focus::Rail,
help: false,
details: false,
status,
last_refreshed: now_clock(),
refreshing: false,
refresh_failed: false,
loading_frame: 0,
flash: false,
scroll: 0,
};
app.set_report(report);
app
}
fn set_report(&mut self, report: Option<UsageReport>) {
self.entries.clear();
if let Some(report) = &report {
for (index, snapshot) in report.accounts.iter().enumerate() {
self.entries.push(NavEntry {
provider: snapshot.provider.clone(),
email: snapshot.account_label.clone(),
health: snapshot_health(snapshot),
snapshot: Some(index),
error: None,
});
}
for (index, error) in report.errors.iter().enumerate() {
self.entries.push(NavEntry {
provider: error.provider.clone(),
email: if error.account_label.is_empty() {
error.account_id.clone()
} else {
error.account_label.clone()
},
health: Health::Error,
snapshot: None,
error: Some(index),
});
}
}
self.report = report;
if self.selected >= self.entries.len() {
self.selected = self.entries.len().saturating_sub(1);
}
self.scroll = 0;
}
fn selected_entry(&self) -> Option<&NavEntry> {
self.entries.get(self.selected)
}
fn move_selection(&mut self, delta: i32) {
if self.entries.is_empty() {
return;
}
let last = self.entries.len() as i32 - 1;
let next = (self.selected as i32 + delta).clamp(0, last);
self.selected = next as usize;
self.details = false;
self.scroll = 0;
}
fn panels(&self, mode: LayoutMode, short: bool) -> Vec<Focus> {
if short {
return vec![Focus::Main];
}
match mode {
LayoutMode::Wide => vec![Focus::Rail, Focus::Main, Focus::Status],
LayoutMode::Medium => vec![Focus::Rail, Focus::Main],
LayoutMode::Narrow => vec![Focus::Main],
}
}
fn cycle_focus(&mut self, delta: i32, mode: LayoutMode, short: bool) {
let panels = self.panels(mode, short);
let current = panels
.iter()
.position(|panel| *panel == self.focus)
.unwrap_or(0);
let count = panels.len() as i32;
let next = ((current as i32 + delta) % count + count) % count;
self.focus = panels[next as usize];
}
}
type UsageLoadResult = std::result::Result<UsageReport, String>;
fn now_clock() -> String {
Local::now().format("%H:%M:%S").to_string()
}
fn loading_glyph(frame: usize) -> &'static str {
["◐", "◓", "◑", "◒"][frame % 4]
}
fn layout_mode(width: u16) -> LayoutMode {
if width >= 110 {
LayoutMode::Wide
} else if width >= 88 {
LayoutMode::Medium
} else {
LayoutMode::Narrow
}
}
fn provider_order(entries: &[NavEntry]) -> Vec<String> {
let mut order: Vec<String> = vec![];
for entry in entries {
if !order.iter().any(|value| value == &entry.provider) {
order.push(entry.provider.clone());
}
}
order
}
fn draw_header(frame: &mut Frame, area: Rect, app: &App) {
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(0)])
.split(area);
let brand = Line::from(vec![
Span::styled(
"\u{25aa} QOTA",
Style::default().fg(c_accent()).add_modifier(Modifier::BOLD),
),
Span::styled(" Local quota dashboard", Style::default().fg(c_faint())),
]);
let mut right = vec![];
if app.flash {
right.push(Span::styled(
"\u{2713} signed in \u{b7} quota verified ",
Style::default().fg(c_ok()),
));
}
right.push(Span::styled(
format!("\u{21bb} {}", app.last_refreshed),
Style::default().fg(c_meta()),
));
let (refresh_text, refresh_color) = if app.refreshing {
(
format!("{} loading", loading_glyph(app.loading_frame)),
c_info(),
)
} else if app.refresh_failed {
(format!("\u{26a0} {}", truncate(&app.status, 40)), c_err())
} else {
("live".to_string(), c_ok())
};
right.push(Span::styled(
format!(" \u{b7} {refresh_text}"),
Style::default().fg(refresh_color),
));
right.push(Span::styled(
format!(" \u{b7} {} accounts", app.entries.len()),
Style::default().fg(c_faint()),
));
let columns = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(rows[0]);
frame.render_widget(Paragraph::new(brand), columns[0]);
frame.render_widget(
Paragraph::new(Line::from(right)).alignment(Alignment::Right),
columns[1],
);
draw_hairline(frame, rows[1]);
}
fn draw_tabs(frame: &mut Frame, area: Rect, app: &App) {
let order = provider_order(&app.entries);
let current = app.selected_entry().map(|entry| entry.provider.clone());
let mut spans = vec![];
for provider in &order {
let active = current.as_deref() == Some(provider.as_str());
let style = if active {
Style::default()
.fg(c_bright())
.add_modifier(Modifier::BOLD | Modifier::UNDERLINED)
} else {
Style::default().fg(c_meta())
};
spans.push(Span::styled(format!(" {} ", title(provider)), style));
spans.push(Span::raw(" "));
}
frame.render_widget(Paragraph::new(Line::from(spans)), area);
}
fn draw_rail(frame: &mut Frame, area: Rect, app: &App, focused: bool) {
let block = Block::default()
.borders(Borders::RIGHT)
.border_style(Style::default().fg(if focused { c_focus() } else { c_hair() }));
let inner = block.inner(area);
frame.render_widget(block, area);
let mut lines = vec![
Line::from(Span::styled(
"ACCOUNTS",
Style::default().fg(c_bright()).add_modifier(Modifier::BOLD),
)),
Line::from(""),
];
if app.entries.is_empty() {
lines.push(Line::from(Span::styled(
"No accounts yet",
Style::default().fg(c_meta()).add_modifier(Modifier::ITALIC),
)));
}
let order = provider_order(&app.entries);
let name_width = inner.width.saturating_sub(5) as usize;
for provider in &order {
lines.push(Line::from(Span::styled(
title(provider),
Style::default().fg(c_dim()).add_modifier(Modifier::BOLD),
)));
for (index, entry) in app.entries.iter().enumerate() {
if &entry.provider != provider {
continue;
}
let selected = index == app.selected;
let bar = if selected { "\u{258c}" } else { " " };
let caret = if selected { "\u{203a}" } else { " " };
let email = truncate(&entry.email, name_width.max(4));
let name_style = if selected {
Style::default()
.fg(c_bright())
.bg(c_sel_bg())
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(c_dim())
};
let row_bg = if selected {
Style::default().bg(c_sel_bg())
} else {
Style::default()
};
lines.push(Line::from(vec![
Span::styled(bar, Style::default().fg(c_accent())),
Span::styled(format!("{caret} "), name_style),
Span::styled(
format!("{email:<width$}", width = name_width.max(4)),
name_style,
),
Span::styled(" ", row_bg),
Span::styled(
health_sym(entry.health),
Style::default().fg(health_color(entry.health)),
),
]));
}
}
let legend = Line::from(vec![
Span::styled("\u{25cf}", Style::default().fg(c_ok())),
Span::styled(" live ", Style::default().fg(c_meta())),
Span::styled("\u{25d0}", Style::default().fg(c_warn())),
Span::styled(" part ", Style::default().fg(c_meta())),
Span::styled("\u{25b2}", Style::default().fg(c_err())),
Span::styled(" err", Style::default().fg(c_meta())),
]);
let add = Line::from(vec![
Span::styled("+ Add account", Style::default().fg(c_accent())),
Span::styled(" n", Style::default().fg(c_faint())),
]);
let body_height = inner.height as usize;
while lines.len() + 3 < body_height {
lines.push(Line::from(""));
}
lines.push(legend);
lines.push(add);
frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner);
}
fn account_header_line(entry: &NavEntry) -> Line<'static> {
let mut spans = vec![
badge(&entry.provider),
Span::raw(" "),
Span::styled(
entry.email.clone(),
Style::default().fg(c_bright()).add_modifier(Modifier::BOLD),
),
Span::styled(
format!(" \u{b7} {}", status_text(entry.health)),
Style::default().fg(health_color(entry.health)),
),
];
let _ = &mut spans;
Line::from(spans)
}
fn empty_main_lines() -> Vec<Line<'static>> {
vec![
Line::from(""),
Line::from(Span::styled(
"No accounts configured",
Style::default().fg(c_bright()).add_modifier(Modifier::BOLD),
)),
Line::from(Span::styled(
"Qota reads AI quota from provider backends running locally on your machine. Nothing is fetched until you add a provider and sign in.",
Style::default().fg(c_text()),
)),
Line::from(""),
Line::from(vec![
Span::styled(" s ", Style::default().fg(c_accent()).bg(c_sel_bg())),
Span::styled(
" Add your first provider ",
Style::default().fg(c_accent()),
),
Span::styled(
"? ",
Style::default().fg(c_dim()).add_modifier(Modifier::BOLD),
),
Span::styled("How Qota works", Style::default().fg(c_meta())),
]),
]
}
fn loading_main_lines(frame: usize) -> Vec<Line<'static>> {
vec![
Line::from(""),
Line::from(Span::styled(
format!("{} Loading live quota", loading_glyph(frame)),
Style::default().fg(c_info()).add_modifier(Modifier::BOLD),
)),
Line::from(Span::styled(
"Contacting the local backend. Setup remains available with s.",
Style::default().fg(c_meta()),
)),
]
}
fn error_main_lines(entry: &NavEntry, error: &ReportError, details: bool) -> Vec<Line<'static>> {
let mut lines = vec![
account_header_line(entry),
Line::from(""),
Line::from(Span::styled(
format!(
"\u{25b2} {} \u{b7} {} \u{2014} {}",
title(&error.provider),
entry.email,
error.message
),
Style::default().fg(c_err()).add_modifier(Modifier::BOLD),
)),
Line::from(Span::styled(
"This account failed to refresh. Your other accounts are unaffected and still visible in the rail.",
Style::default().fg(c_meta()),
)),
Line::from(""),
Line::from(vec![
Span::styled(
"r",
Style::default().fg(c_info()).add_modifier(Modifier::BOLD),
),
Span::styled(" Retry ", Style::default().fg(c_info())),
Span::styled(
"s",
Style::default().fg(c_dim()).add_modifier(Modifier::BOLD),
),
Span::styled(" Reconfigure ", Style::default().fg(c_meta())),
Span::styled(
"d",
Style::default().fg(c_dim()).add_modifier(Modifier::BOLD),
),
Span::styled(
if details {
" Hide details"
} else {
" Show details"
},
Style::default().fg(c_meta()),
),
]),
];
if details {
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
"Diagnostics (sanitized)",
Style::default().fg(c_meta()),
)));
lines.push(Line::from(Span::styled(
format!(
"provider {} \u{b7} account {}",
error.provider, error.account_id
),
Style::default().fg(c_faint()),
)));
lines.push(Line::from(Span::styled(
"No tokens, URLs, headers, or stack traces are shown.",
Style::default().fg(c_faint()),
)));
}
lines
}
fn codex_compact_lines(entry: &NavEntry, snapshot: &AccountSnapshot) -> Vec<Line<'static>> {
let mut lines = vec![account_header_line(entry), Line::from("")];
for window in &snapshot.windows {
let mut spans = vec![Span::styled(
format!("{:<26}", truncate(&window.label, 26)),
Style::default().fg(c_dim()),
)];
if let Some(used) = window.percent_used {
spans.push(Span::styled(
format!("{:<9}", format!("{} used", fmt_pct(used))),
Style::default()
.fg(threshold_color(used))
.add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(
format!("{:<15}", format!("{} remaining", fmt_pct(100.0 - used))),
Style::default().fg(c_faint()),
));
spans.extend(text_bar(used / 100.0, 18, threshold_color(used)));
spans.push(Span::raw(" "));
} else {
spans.push(Span::styled(
format!("{:<24}", window_state_text(window)),
Style::default().fg(c_meta()),
));
}
if let Some(reset) = reset_label(window.resets_at.as_deref()) {
spans.push(Span::styled(
format!("\u{27f3} {reset}"),
Style::default().fg(c_faint()),
));
}
lines.push(Line::from(spans));
}
lines
}
fn quota_card_rects(area: Rect, count: usize) -> Vec<Rect> {
let card_area = Rect::new(area.x, area.y, area.width, area.height.min(7));
if count > 1 && area.width >= 82 {
let constraints: Vec<Constraint> = (0..count)
.map(|_| Constraint::Ratio(1, count as u32))
.collect();
Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.split(card_area)
.iter()
.enumerate()
.map(|(index, rect)| {
if index + 1 < count {
Rect::new(rect.x, rect.y, rect.width.saturating_sub(1), rect.height)
} else {
*rect
}
})
.collect()
} else {
let constraints: Vec<Constraint> = (0..count).map(|_| Constraint::Length(7)).collect();
Layout::default()
.direction(Direction::Vertical)
.constraints(constraints)
.split(card_area)
.to_vec()
}
}
fn draw_quota_card(
frame: &mut Frame,
area: Rect,
window: &QuotaWindow,
title: String,
explanation: &str,
) {
let card = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(c_hair()));
let inner = card.inner(area);
frame.render_widget(card, area);
let card_rows = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
])
.split(inner);
let (value_color, band) = match window.percent_used {
Some(used) => (threshold_color(used), band_label(used)),
None => (c_meta(), "no data"),
};
let title_width = inner.width as usize;
let band_text = band.to_uppercase();
let gap = title_width.saturating_sub(title.chars().count() + band_text.chars().count());
let title_spans = vec![
Span::styled(
truncate(
&title,
title_width.saturating_sub(band_text.chars().count() + 1),
),
Style::default().fg(c_dim()).add_modifier(Modifier::BOLD),
),
Span::raw(" ".repeat(gap.max(1))),
Span::styled(band_text, Style::default().fg(value_color)),
];
frame.render_widget(Paragraph::new(Line::from(title_spans)), card_rows[0]);
let headline = match window.percent_used {
Some(used) => Line::from(vec![
Span::styled(
format!("{} used", fmt_pct(used)),
Style::default()
.fg(value_color)
.add_modifier(Modifier::BOLD),
),
Span::styled(
format!(" {} remaining", fmt_pct(100.0 - used)),
Style::default().fg(c_faint()),
),
]),
None => Line::from(Span::styled(
window_state_text(window),
Style::default().fg(c_meta()),
)),
};
frame.render_widget(Paragraph::new(headline), card_rows[1]);
if let Some(used) = window.percent_used {
frame.render_widget(
Paragraph::new(Line::from(text_bar(
used / 100.0,
card_rows[2].width as usize,
value_color,
))),
card_rows[2],
);
}
if let Some(reset) = reset_label(window.resets_at.as_deref()) {
frame.render_widget(
Paragraph::new(Line::from(Span::styled(
format!("\u{27f3} {reset}"),
Style::default().fg(c_meta()),
))),
card_rows[3],
);
}
frame.render_widget(
Paragraph::new(Line::from(Span::styled(
explanation.to_string(),
Style::default().fg(c_faint()),
)))
.wrap(Wrap { trim: false }),
card_rows[4],
);
}
fn draw_codex_cards(frame: &mut Frame, area: Rect, entry: &NavEntry, snapshot: &AccountSnapshot) {
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
])
.split(area);
frame.render_widget(Paragraph::new(account_header_line(entry)), rows[0]);
for (window, card_area) in snapshot
.windows
.iter()
.zip(quota_card_rects(rows[2], snapshot.windows.len()))
{
draw_quota_card(
frame,
card_area,
window,
codex_window_title(&window.key, &window.label),
codex_explanation(&window.key),
);
}
}
fn antigravity_card_groups(
snapshot: &AccountSnapshot,
) -> Vec<(String, Option<String>, Vec<&QuotaWindow>)> {
let mut groups: Vec<(String, Option<String>, Vec<&QuotaWindow>)> = vec![];
for window in &snapshot.windows {
let name = window
.group
.clone()
.unwrap_or_else(|| "Antigravity Models".to_string());
if groups.last().is_none_or(|group| group.0 != name) {
groups.push((name, window.group_description.clone(), vec![]));
}
groups.last_mut().expect("group inserted").2.push(window);
}
groups
}
fn draw_antigravity_cards(
frame: &mut Frame,
area: Rect,
entry: &NavEntry,
snapshot: &AccountSnapshot,
) {
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1),
Constraint::Length(1),
Constraint::Min(0),
])
.split(area);
frame.render_widget(Paragraph::new(account_header_line(entry)), rows[0]);
let content = rows[2];
let mut y = content.y;
for (group, description, windows) in antigravity_card_groups(snapshot) {
if y >= content.bottom() {
break;
}
frame.render_widget(
Paragraph::new(Line::from(Span::styled(
group,
Style::default().fg(c_bright()).add_modifier(Modifier::BOLD),
))),
Rect::new(content.x, y, content.width, 1),
);
y = y.saturating_add(1);
draw_hairline(frame, Rect::new(content.x, y, content.width, 1));
y = y.saturating_add(1);
if let Some(description) = &description {
frame.render_widget(
Paragraph::new(Line::from(Span::styled(
truncate(description, content.width as usize),
Style::default().fg(c_faint()),
))),
Rect::new(content.x, y, content.width, 1),
);
y = y.saturating_add(1);
}
let window_count = windows.len();
let card_height = if window_count > 1 && content.width >= 82 {
7
} else {
7 * window_count as u16
};
let card_area = Rect::new(
content.x,
y,
content.width,
card_height.min(content.bottom().saturating_sub(y)),
);
for (window, card) in windows
.into_iter()
.zip(quota_card_rects(card_area, window_count))
{
draw_quota_card(
frame,
card,
window,
window.label.clone(),
"Provider-defined shared quota pool.",
);
}
y = y.saturating_add(card_height + 1);
}
}
fn draw_main_lines(frame: &mut Frame, area: Rect, lines: Vec<Line<'static>>, scroll: u16) {
let total = lines.len() as u16;
let visible = area.height;
let max_scroll = total.saturating_sub(visible);
let offset = scroll.min(max_scroll);
frame.render_widget(
Paragraph::new(lines)
.wrap(Wrap { trim: false })
.scroll((offset, 0)),
area,
);
if total > visible {
let mut state = ScrollbarState::new(total as usize).position(offset as usize);
frame.render_stateful_widget(
Scrollbar::new(ScrollbarOrientation::VerticalRight)
.thumb_style(Style::default().fg(c_track())),
area,
&mut state,
);
}
}
fn draw_main(frame: &mut Frame, area: Rect, app: &App, short: bool) {
let Some(entry) = app.selected_entry() else {
let lines = if app.refreshing && app.report.is_none() {
loading_main_lines(app.loading_frame)
} else {
empty_main_lines()
};
draw_main_lines(frame, area, lines, app.scroll);
return;
};
let report = app.report.as_ref();
if let Some(error_index) = entry.error
&& let Some(error) = report.and_then(|report| report.errors.get(error_index))
{
draw_main_lines(
frame,
area,
error_main_lines(entry, error, app.details),
app.scroll,
);
return;
}
let snapshot = entry
.snapshot
.and_then(|index| report.and_then(|report| report.accounts.get(index)));
let Some(snapshot) = snapshot else {
draw_main_lines(frame, area, empty_main_lines(), app.scroll);
return;
};
if !short && entry.provider.eq_ignore_ascii_case("antigravity") {
draw_antigravity_cards(frame, area, entry, snapshot);
} else if !short {
draw_codex_cards(frame, area, entry, snapshot);
} else {
draw_main_lines(
frame,
area,
codex_compact_lines(entry, snapshot),
app.scroll,
);
}
}
fn draw_status(frame: &mut Frame, area: Rect, app: &App, focused: bool) {
let block = Block::default()
.borders(Borders::LEFT)
.border_style(Style::default().fg(if focused { c_focus() } else { c_hair() }));
let inner = block.inner(area);
frame.render_widget(block, area);
let mut lines = vec![
Line::from(Span::styled(
"STATUS",
Style::default().fg(c_bright()).add_modifier(Modifier::BOLD),
)),
Line::from(""),
];
if app.entries.is_empty() {
lines.push(Line::from(Span::styled(
if app.refreshing {
format!("{} Loading accounts…", loading_glyph(app.loading_frame))
} else {
"Nothing to monitor yet.".to_string()
},
Style::default()
.fg(if app.refreshing { c_info() } else { c_meta() })
.add_modifier(Modifier::ITALIC),
)));
frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner);
return;
}
for provider in provider_order(&app.entries) {
let group: Vec<&NavEntry> = app
.entries
.iter()
.filter(|entry| entry.provider == provider)
.collect();
let live = group.iter().filter(|e| e.health == Health::Live).count();
let partial = group.iter().filter(|e| e.health == Health::Partial).count();
let errored = group.iter().filter(|e| e.health == Health::Error).count();
let mut parts = vec![format!("{live} live")];
if partial > 0 {
parts.push(format!("{partial} partial"));
}
if errored > 0 {
parts.push(format!("{errored} error"));
}
let color = if errored > 0 {
c_err()
} else if partial > 0 {
c_warn()
} else {
c_ok()
};
lines.push(Line::from(vec![
Span::styled(
format!("{:<12}", title(&provider)),
Style::default().fg(c_dim()),
),
Span::styled(parts.join(" \u{b7} "), Style::default().fg(color)),
]));
}
let attention: Vec<&NavEntry> = app
.entries
.iter()
.filter(|entry| entry.health != Health::Live)
.collect();
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
format!("ATTENTION \u{b7} {}", attention.len()),
Style::default().fg(c_meta()).add_modifier(Modifier::BOLD),
)));
if attention.is_empty() {
lines.push(Line::from(Span::styled(
"\u{25cf} All accounts healthy",
Style::default().fg(c_ok()),
)));
} else {
for entry in attention {
let color = health_color(entry.health);
lines.push(Line::from(vec![
Span::styled(
format!("{} ", health_sym(entry.health)),
Style::default().fg(color),
),
Span::styled(
format!(
"{} \u{b7} {}",
title(&entry.provider),
truncate(&entry.email, 16)
),
Style::default().fg(color),
),
]));
lines.push(Line::from(Span::styled(
if entry.health == Health::Error {
" quota unavailable"
} else {
" partial provider data"
},
Style::default().fg(c_meta()),
)));
}
}
let body_height = inner.height as usize;
while lines.len() + 2 < body_height {
lines.push(Line::from(""));
}
lines.push(Line::from(Span::styled(
format!("Last refreshed {}", app.last_refreshed),
Style::default().fg(c_faint()),
)));
lines.push(Line::from(Span::styled(
"Auto-refresh: press r",
Style::default().fg(c_faint()),
)));
frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner);
}
fn footer_hints(app: &App, width: u16) -> &'static [(&'static str, &'static str)] {
if app.help {
&[("Esc", "Close"), ("?", "Close")]
} else if width < 88 {
&[
("r", "Refresh"),
("n", "Add account"),
("\u{2191}\u{2193}", "Select"),
("?", "Help"),
("q", "Quit"),
]
} else {
&[
("r", "Refresh"),
("\u{2191}\u{2193}", "Select"),
("Tab", "Panel"),
("?", "Help"),
("q", "Quit"),
]
}
}
fn draw_footer(frame: &mut Frame, area: Rect, app: &App) {
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(0)])
.split(area);
draw_hairline(frame, rows[0]);
let hints = footer_hints(app, area.width);
let mut spans = vec![];
for (key, label) in hints {
spans.push(Span::styled(
*key,
Style::default().fg(c_accent()).add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(
format!(" {label} "),
Style::default().fg(c_meta()),
));
}
frame.render_widget(Paragraph::new(Line::from(spans)), rows[1]);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DashboardMouseAction {
None,
Refresh,
Setup,
Quit,
}
fn rail_entry_at(app: &App, area: Rect, column: u16, row: u16) -> Option<usize> {
if column < area.x || column >= area.right().saturating_sub(1) {
return None;
}
let mut line = area.y.saturating_add(2); for provider in provider_order(&app.entries) {
if row == line {
return app
.entries
.iter()
.position(|entry| entry.provider == provider);
}
line = line.saturating_add(1); for (index, entry) in app.entries.iter().enumerate() {
if entry.provider != provider {
continue;
}
if row == line {
return Some(index);
}
line = line.saturating_add(1);
}
}
None
}
fn tab_entry_at(app: &App, area: Rect, column: u16) -> Option<usize> {
let mut x = area.x;
for provider in provider_order(&app.entries) {
let width = title(&provider).chars().count() as u16 + 3;
if column >= x && column < x.saturating_add(width) {
return app
.entries
.iter()
.position(|entry| entry.provider == provider);
}
x = x.saturating_add(width + 1);
}
None
}
fn footer_action_at(app: &App, area: Rect, column: u16, row: u16) -> DashboardMouseAction {
if row != area.bottom().saturating_sub(1) || app.help {
return DashboardMouseAction::None;
}
let mut x = area.x;
for (key, label) in footer_hints(app, area.width) {
let width = (key.chars().count() + label.chars().count() + 4) as u16;
if column >= x && column < x.saturating_add(width) {
return match *key {
"r" => DashboardMouseAction::Refresh,
"q" => DashboardMouseAction::Quit,
"?" => DashboardMouseAction::None,
_ => DashboardMouseAction::None,
};
}
x = x.saturating_add(width);
}
DashboardMouseAction::None
}
fn handle_dashboard_mouse(app: &mut App, mouse: MouseEvent, area: Rect) -> DashboardMouseAction {
let short = area.height <= 18;
let mode = layout_mode(area.width);
match mouse.kind {
MouseEventKind::ScrollDown => {
if short {
app.scroll = app.scroll.saturating_add(1);
} else {
app.move_selection(1);
}
return DashboardMouseAction::None;
}
MouseEventKind::ScrollUp => {
if short {
app.scroll = app.scroll.saturating_sub(1);
} else {
app.move_selection(-1);
}
return DashboardMouseAction::None;
}
MouseEventKind::Down(MouseButton::Left) => {}
_ => return DashboardMouseAction::None,
}
if app.help {
app.help = false;
return DashboardMouseAction::None;
}
let footer_y = area.y.saturating_add(area.height.saturating_sub(1));
let footer = Rect::new(
area.x,
area.y
.saturating_add(area.height.saturating_sub(DASHBOARD_FOOTER_HEIGHT)),
area.width,
DASHBOARD_FOOTER_HEIGHT,
);
let action = footer_action_at(app, footer, mouse.column, mouse.row);
if action != DashboardMouseAction::None {
return action;
}
if mouse.row == footer_y && mouse.column >= area.x && mouse.column < area.right() {
let hints = footer_hints(app, area.width);
let mut x = area.x;
for (key, label) in hints {
let width = (key.chars().count() + label.chars().count() + 4) as u16;
if mouse.column >= x && mouse.column < x.saturating_add(width) {
if *key == "?" {
app.help = true;
}
return DashboardMouseAction::None;
}
x = x.saturating_add(width);
}
}
let body_y = area
.y
.saturating_add(DASHBOARD_HEADER_HEIGHT + u16::from(mode == LayoutMode::Narrow && !short));
let body_height = area
.height
.saturating_sub(body_y - area.y + DASHBOARD_FOOTER_HEIGHT);
let body = Rect::new(area.x, body_y, area.width, body_height);
if !short && matches!(mode, LayoutMode::Wide | LayoutMode::Medium) {
let rail = Rect::new(body.x, body.y, body.width.min(24), body.height);
if rail.contains((mouse.column, mouse.row).into())
&& mouse.row >= rail.bottom().saturating_sub(2)
{
return DashboardMouseAction::Setup;
}
if let Some(index) = rail_entry_at(app, rail, mouse.column, mouse.row) {
app.selected = index;
app.focus = Focus::Rail;
app.details = false;
app.scroll = 0;
}
} else if mode == LayoutMode::Narrow
&& !short
&& mouse.row == area.y.saturating_add(DASHBOARD_HEADER_HEIGHT)
{
let tabs = Rect::new(
area.x,
area.y.saturating_add(DASHBOARD_HEADER_HEIGHT),
area.width,
1,
);
if let Some(index) = tab_entry_at(app, tabs, mouse.column) {
app.selected = index;
app.details = false;
app.scroll = 0;
}
} else if body.contains((mouse.column, mouse.row).into())
&& app.selected_entry().map(|entry| entry.health) == Some(Health::Error)
{
app.details = !app.details;
}
DashboardMouseAction::None
}
fn draw_help(frame: &mut Frame, area: Rect) {
let modal = centered_rect(60, 60, area);
frame.render_widget(Clear, modal);
let block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(c_focus()))
.title(Span::styled(
" Controls ",
Style::default().fg(c_accent()).add_modifier(Modifier::BOLD),
));
let inner = block.inner(modal);
frame.render_widget(block, modal);
let keys = [
("\u{2191} / \u{2193}", "Select account / model"),
("Tab", "Switch panel"),
("Enter", "Open details / action"),
("r", "Refresh"),
("n / +", "Add account"),
("?", "Help"),
("q", "Quit"),
("Click", "Select accounts and actions"),
("Wheel", "Move selection / scroll"),
];
let mut lines = vec![];
for (key, desc) in keys {
lines.push(Line::from(vec![
Span::styled(format!("{key:<10}"), Style::default().fg(c_accent())),
Span::styled(desc, Style::default().fg(c_dim())),
]));
}
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
"Quota values come from provider/backend responses. Qota does",
Style::default().fg(c_meta()),
)));
lines.push(Line::from(Span::styled(
"not estimate usage when providers omit it.",
Style::default().fg(c_meta()),
)));
frame.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner);
}
fn draw_dashboard(frame: &mut Frame, app: &App) {
let area = frame.area();
let short = area.height <= 18;
let mode = layout_mode(area.width);
let narrow_tabs = mode == LayoutMode::Narrow && !short;
let mut vertical = vec![Constraint::Length(DASHBOARD_HEADER_HEIGHT)];
if narrow_tabs {
vertical.push(Constraint::Length(1));
}
vertical.push(Constraint::Min(0));
vertical.push(Constraint::Length(DASHBOARD_FOOTER_HEIGHT));
let rows = Layout::default()
.direction(Direction::Vertical)
.constraints(vertical)
.split(area);
draw_header(frame, rows[0], app);
let body_index = if narrow_tabs {
draw_tabs(frame, rows[1], app);
2
} else {
1
};
let footer_index = rows.len() - 1;
let body = rows[body_index];
let show_rail = !short && matches!(mode, LayoutMode::Wide | LayoutMode::Medium);
let show_status = !short && mode == LayoutMode::Wide;
let mut constraints = vec![];
if show_rail {
constraints.push(Constraint::Length(24));
}
constraints.push(Constraint::Min(0));
if show_status {
constraints.push(Constraint::Length(34));
}
let panels = Layout::default()
.direction(Direction::Horizontal)
.constraints(constraints)
.split(body);
let mut cursor = 0;
if show_rail {
draw_rail(frame, panels[cursor], app, app.focus == Focus::Rail);
cursor += 1;
}
let main_area = panels[cursor];
draw_main(frame, main_area, app, short);
cursor += 1;
if show_status {
draw_status(frame, panels[cursor], app, app.focus == Focus::Status);
}
draw_footer(frame, rows[footer_index], app);
if app.help {
draw_help(frame, area);
}
}
fn start_usage_load(config_path: PathBuf, sender: mpsc::Sender<UsageLoadResult>) {
thread::spawn(move || {
let result = read_cpa_config(&config_path)
.and_then(|config| collect_usage(&config, None, 15_000))
.map_err(|error| error.to_string());
let _ = sender.send(result);
});
}
fn apply_usage_load(app: &mut App, result: UsageLoadResult) {
match result {
Ok(report) => {
app.set_report(Some(report));
app.status = "Live quota loaded".to_string();
app.refresh_failed = false;
}
Err(error) => {
app.status = format!("Live quota unavailable: {error}. Press s to connect providers.");
app.refresh_failed = true;
}
}
app.last_refreshed = now_clock();
app.refreshing = false;
}
fn dashboard(config_path: PathBuf, override_interval: Option<u64>) -> Result<()> {
let mut app = App::new(None, "Loading live quota…".to_string());
app.refreshing = true;
let (sender, receiver) = mpsc::channel();
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
start_usage_load(config_path.clone(), sender.clone());
let result = run_dashboard(
&mut terminal,
&mut app,
&config_path,
&receiver,
&sender,
override_interval,
);
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
DisableMouseCapture,
LeaveAlternateScreen
)?;
terminal.show_cursor()?;
result
}
fn run_add_account(
terminal: &mut AppTerminal,
app: &mut App,
config_path: &Path,
sender: &mpsc::Sender<UsageLoadResult>,
) -> Result<()> {
match setup_tui(terminal, config_path, None) {
Ok(Some(message)) => {
show_dashboard_return_countdown(terminal, &message)?;
app.status = message;
app.flash = true;
app.refresh_failed = false;
refresh_dashboard(app, config_path, sender);
}
Ok(None) => app.status = "Setup cancelled".to_string(),
Err(error) => {
app.status = error.to_string();
app.refresh_failed = true;
}
}
Ok(())
}
fn refresh_dashboard(app: &mut App, config_path: &Path, sender: &mpsc::Sender<UsageLoadResult>) {
if app.refreshing {
return;
}
app.refreshing = true;
app.refresh_failed = false;
app.status = "Loading live quota…".to_string();
start_usage_load(config_path.to_path_buf(), sender.clone());
}
fn run_dashboard(
terminal: &mut AppTerminal,
app: &mut App,
config_path: &Path,
receiver: &mpsc::Receiver<UsageLoadResult>,
sender: &mpsc::Sender<UsageLoadResult>,
override_interval: Option<u64>,
) -> Result<()> {
let get_refresh_interval = || -> u64 {
if let Some(val) = override_interval {
val
} else if let Ok(config) = read_cpa_config(config_path) {
config.refresh_interval
} else {
60
}
};
let mut last_refresh_trigger = Instant::now();
loop {
while let Ok(result) = receiver.try_recv() {
apply_usage_load(app, result);
}
terminal.draw(|frame| draw_dashboard(frame, app))?;
let size = terminal.size()?;
let area = Rect::new(0, 0, size.width, size.height);
let short = area.height <= 18;
let mode = layout_mode(area.width);
let interval = get_refresh_interval();
if interval > 0 && last_refresh_trigger.elapsed() >= Duration::from_secs(interval) {
last_refresh_trigger = Instant::now();
refresh_dashboard(app, config_path, sender);
}
if !event::poll(Duration::from_millis(100))? {
if app.refreshing {
app.loading_frame = app.loading_frame.wrapping_add(1);
}
continue;
}
match event::read()? {
Event::Mouse(mouse) => match handle_dashboard_mouse(app, mouse, area) {
DashboardMouseAction::None => {}
DashboardMouseAction::Refresh => {
last_refresh_trigger = Instant::now();
refresh_dashboard(app, config_path, sender);
}
DashboardMouseAction::Setup => run_add_account(terminal, app, config_path, sender)?,
DashboardMouseAction::Quit => break,
},
Event::Key(key) => {
if key.kind != KeyEventKind::Press {
continue;
}
if app.help {
if matches!(
key.code,
KeyCode::Esc | KeyCode::Char('q') | KeyCode::Char('?')
) {
app.help = false;
}
continue;
}
match key.code {
KeyCode::Char('q') => break,
KeyCode::Char('?') => app.help = true,
KeyCode::Char('r') => {
last_refresh_trigger = Instant::now();
refresh_dashboard(app, config_path, sender);
}
KeyCode::Char('n') | KeyCode::Char('+') => {
run_add_account(terminal, app, config_path, sender)?
}
KeyCode::Down => {
if short {
app.scroll = app.scroll.saturating_add(1);
} else {
app.move_selection(1);
}
}
KeyCode::Up => {
if short {
app.scroll = app.scroll.saturating_sub(1);
} else {
app.move_selection(-1);
}
}
KeyCode::PageDown => app.scroll = app.scroll.saturating_add(5),
KeyCode::PageUp => app.scroll = app.scroll.saturating_sub(5),
KeyCode::Tab => app.cycle_focus(1, mode, short),
KeyCode::BackTab => app.cycle_focus(-1, mode, short),
KeyCode::Enter => {
if app.selected_entry().map(|entry| entry.health) == Some(Health::Error) {
app.details = !app.details;
}
}
KeyCode::Esc => {
app.flash = false;
}
_ => {}
}
}
_ => {}
}
}
Ok(())
}
fn truncate(value: &str, max: usize) -> String {
let count = value.chars().count();
if count <= max {
value.to_string()
} else if max == 0 {
String::new()
} else {
let kept: String = value.chars().take(max - 1).collect();
format!("{kept}\u{2026}")
}
}
fn parse_providers(value: &str) -> Result<Vec<String>> {
let providers: Vec<_> = value
.split(',')
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToString::to_string)
.collect();
if providers.is_empty()
|| providers
.iter()
.any(|provider| !matches!(provider.as_str(), "claude" | "codex" | "antigravity"))
{
bail!("--providers supports claude, codex, or antigravity");
}
Ok(providers)
}
fn run_background_poll(args: &Commands) -> Result<()> {
let Commands::Poll {
interval,
once,
json,
provider,
output,
config,
timeout,
..
} = args
else {
unreachable!()
};
let executable = env::current_exe()?;
let mut command = ProcessCommand::new(executable);
command
.arg("poll")
.arg("--interval")
.arg(interval.to_string())
.arg("--output")
.arg(output_path(output.clone())?)
.arg("--timeout")
.arg(timeout.to_string());
if *once {
command.arg("--once");
}
if *json {
command.arg("--json");
}
if let Some(provider) = provider {
command.arg("--provider").arg(provider);
}
if let Some(config) = config {
command.arg("--config").arg(config);
}
command
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()?;
println!("Started poll process");
Ok(())
}
fn main() -> Result<()> {
let cli = Cli::parse();
match cli.command {
None if io::stdout().is_terminal() => {
dashboard(config_path(cli.config)?, cli.refresh_interval)
}
None => {
let report = collect_usage(&read_cpa_config(&config_path(None)?)?, None, 15_000)?;
print!("{}", human_report(&report));
Ok(())
}
Some(Commands::Setup { providers, config }) => setup_interactive(
config_path(config)?,
providers.as_deref().map(parse_providers).transpose()?,
),
Some(Commands::List {
json,
provider,
config,
timeout,
}) => {
let report = collect_usage(
&read_cpa_config(&config_path(config)?)?,
provider.as_deref(),
timeout,
)?;
if json {
println!("{}", serde_json::to_string(&report)?);
} else {
print!("{}", human_report(&report));
}
Ok(())
}
Some(
command @ Commands::Poll {
background: true, ..
},
) => run_background_poll(&command),
Some(Commands::Poll {
interval,
once,
json,
provider,
output,
config,
timeout,
..
}) => run_poll(
read_cpa_config(&config_path(config)?)?,
interval,
once,
json,
provider,
output_path(output)?,
timeout,
),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::KeyModifiers;
use ratatui::backend::TestBackend;
#[test]
fn parses_codex_rolling_windows() {
let windows = parse_codex_usage(
&json!({"rate_limit":{"primary_window":{"limit_window_seconds":18000,"used_percent":25,"reset_at":1785282029},"secondary_window":{"limit_window_seconds":604800,"used_percent":47,"reset_at":1785282029}}}),
);
assert_eq!(
windows
.iter()
.map(|window| window.key.as_str())
.collect::<Vec<_>>(),
["5h", "7d"]
);
assert_eq!(windows[0].remaining_percent, Some(75.0));
}
#[test]
fn parses_claude_rolling_and_model_windows() {
let windows = parse_claude_usage(&json!({
"five_hour": {"utilization": 14, "resets_at": "2026-07-23T12:00:00Z"},
"seven_day": {"utilization": 41, "resets_at": "2026-07-29T12:00:00Z"},
"seven_day_opus": null,
"seven_day_sonnet": {"utilization": 8, "resets_at": "2026-07-29T12:00:00Z"}
}));
assert_eq!(
windows
.iter()
.map(|window| window.key.as_str())
.collect::<Vec<_>>(),
["5h", "7d", "7d-sonnet"]
);
assert_eq!(windows[0].percent_used, Some(14.0));
assert_eq!(windows[1].remaining_percent, Some(59.0));
}
#[test]
fn parses_antigravity_native_quota_groups() {
let windows = parse_antigravity_usage(&json!({
"groups": [
{
"displayName": "Gemini Models",
"description": "Models within this group: Gemini Flash, Gemini Pro",
"buckets": [
{"bucketId": "gemini-weekly", "displayName": "Weekly Limit", "remainingFraction": 0.9808, "resetTime": "2026-07-28T10:00:00Z"},
{"bucketId": "gemini-5h", "displayName": "Five Hour Limit", "remainingFraction": 0.959, "resetTime": "2026-07-23T14:30:00Z"}
]
},
{
"displayName": "Claude and GPT models",
"buckets": [
{"bucketId": "3p-weekly", "displayName": "Weekly Limit", "remainingFraction": 0, "resetTime": "2026-07-28T10:00:00Z"},
{"bucketId": "3p-5h", "displayName": "Five Hour Limit", "disabled": true, "remainingFraction": 1}
]
}
]
}));
assert_eq!(windows.len(), 3);
assert_eq!(windows[0].group.as_deref(), Some("Gemini Models"));
assert_eq!(
windows[0].group_description.as_deref(),
Some("Models within this group: Gemini Flash, Gemini Pro")
);
assert_eq!(windows[0].key, "Gemini Models:gemini-weekly");
assert_eq!(windows[0].remaining_percent, Some(98.08));
assert!((windows[0].percent_used.unwrap() - 1.92).abs() < 0.000_001);
assert_eq!(windows[1].label, "Five Hour Limit");
assert_eq!(windows[2].group.as_deref(), Some("Claude and GPT models"));
assert_eq!(windows[2].percent_used, Some(100.0));
}
#[test]
fn bad_credentials_network_and_malformed_accounts_are_isolated() {
for failure in [
"invalid authentication credentials",
"request timed out after 15000ms",
"CLIProxyAPI api-call returned invalid JSON (200)",
] {
let good = Account {
id: "good".into(),
label: "good".into(),
provider: "codex".into(),
auth_index: "good".into(),
raw: json!({}),
};
let bad = Account {
id: "bad".into(),
label: "bad".into(),
provider: "antigravity".into(),
auth_index: "bad".into(),
raw: json!({}),
};
let good_snapshot = AccountSnapshot {
provider: "codex".into(),
account_id: "good".into(),
account_label: "good".into(),
fetched_at: now(),
windows: vec![],
};
let mut report = UsageReport {
version: 1,
generated_at: now(),
accounts: vec![],
errors: vec![],
};
record_account_result(&mut report, &good, Ok(good_snapshot));
record_account_result(
&mut report,
&bad,
Err(ProviderError::new("antigravity", failure).into()),
);
assert_eq!(report.accounts.len(), 1, "{failure}");
assert_eq!(report.errors.len(), 1, "{failure}");
assert_eq!(report.errors[0].account_id, "bad", "{failure}");
assert_eq!(report.errors[0].message, failure);
}
}
#[test]
fn dashboard_renders_report() {
let backend = TestBackend::new(100, 30);
let mut terminal = Terminal::new(backend).unwrap();
let report = UsageReport {
version: 1,
generated_at: now(),
accounts: vec![AccountSnapshot {
provider: "codex".into(),
account_id: "main".into(),
account_label: "primary".into(),
fetched_at: now(),
windows: vec![quota_window(
"7d",
"7 day",
None,
vec![],
Some(53.0),
None,
Some(now()),
)],
}],
errors: vec![],
};
let app = App::new(Some(report), "Live quota loaded".into());
terminal.draw(|frame| draw_dashboard(frame, &app)).unwrap();
let output = terminal
.backend()
.buffer()
.content
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(output.contains("QOTA"));
assert!(output.contains("Codex"));
assert!(output.contains("Refresh"));
assert!(output.contains("Add account"));
assert!(!output.contains("Open OpenAI Platform"));
}
#[test]
fn refresh_failure_is_visible_in_header() {
let backend = TestBackend::new(120, 30);
let mut terminal = Terminal::new(backend).unwrap();
let mut app = App::new(None, "loaded".into());
app.status = "sidecar unavailable".to_string();
app.refresh_failed = true;
terminal.draw(|frame| draw_dashboard(frame, &app)).unwrap();
let output = terminal
.backend()
.buffer()
.content
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(output.contains("sidecar unavailable"));
}
#[test]
fn dashboard_shows_loading_before_usage_arrives() {
let backend = TestBackend::new(120, 30);
let mut terminal = Terminal::new(backend).unwrap();
let mut app = App::new(None, "Loading live quota…".into());
app.refreshing = true;
terminal.draw(|frame| draw_dashboard(frame, &app)).unwrap();
let output = terminal
.backend()
.buffer()
.content
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(output.contains("loading"));
assert!(output.contains("Loading live quota"));
assert!(output.contains("Loading accounts"));
}
#[test]
fn remaining_fraction_never_renders_used_or_gauge() {
let backend = TestBackend::new(120, 30);
let mut terminal = Terminal::new(backend).unwrap();
let report = UsageReport {
version: 1,
generated_at: now(),
accounts: vec![AccountSnapshot {
provider: "antigravity".into(),
account_id: "ag".into(),
account_label: "ops@northwind.dev".into(),
fetched_at: now(),
windows: vec![QuotaWindow {
key: "gemini-3.6-flash".into(),
label: "Gemini 3.6 Flash".into(),
group: Some("Gemini".into()),
group_description: None,
efforts: Some(vec!["low".into(), "high".into()]),
percent_used: None,
remaining_percent: Some(80.0),
resets_at: Some(now()),
}],
}],
errors: vec![],
};
let app = App::new(Some(report), "loaded".into());
terminal.draw(|frame| draw_dashboard(frame, &app)).unwrap();
let output = terminal
.backend()
.buffer()
.content
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(output.contains("Provider reports 80% remaining"));
assert!(!output.contains("used"));
assert!(!output.contains('\u{2588}'));
}
#[test]
fn oauth_output_hides_sensitive_values_and_typed_input_renders() {
assert_eq!(
safe_oauth_line("access_token=private"),
"Sensitive provider output hidden"
);
assert_eq!(safe_oauth_line("Browser opened"), "Browser opened");
let backend = TestBackend::new(100, 30);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|frame| draw_oauth_tui(frame, "codex", &["Browser opened".into()], "1234"))
.unwrap();
let output = terminal
.backend()
.buffer()
.content
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(output.contains("Typed input"));
assert!(output.contains("Approve provider account"));
assert!(output.contains("••••"));
assert!(!output.contains("1234"));
}
#[test]
fn completion_screen_explains_dashboard_return_countdown() {
let backend = TestBackend::new(100, 30);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|frame| draw_dashboard_return_tui(frame, "Qota is ready.", 5))
.unwrap();
let output = terminal
.backend()
.buffer()
.content
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(output.contains("PROVIDERS CONNECTED"));
assert!(output.contains("Returning to dashboard in 5s"));
assert!(output.contains("Live quota will load there"));
assert_eq!(countdown_seconds(Instant::now()), 0);
}
#[test]
fn multi_window_cards_stay_compact() {
let cards = quota_card_rects(Rect::new(0, 0, 100, 20), 2);
assert_eq!(cards.len(), 2);
assert!(cards.iter().all(|card| card.height == 7));
}
#[test]
fn footer_keeps_only_core_dashboard_shortcuts() {
let app = App::new(None, "loaded".into());
let hints = footer_hints(&app, 120);
assert!(hints.contains(&("r", "Refresh")));
assert!(hints.contains(&("\u{2191}\u{2193}", "Select")));
assert!(hints.contains(&("Tab", "Panel")));
assert!(hints.contains(&("?", "Help")));
assert!(hints.contains(&("q", "Quit")));
assert!(
!hints
.iter()
.any(|(_, label)| *label == "Accounts" || *label == "Setup" || *label == "Details")
);
}
#[test]
fn setup_picker_is_vertical_and_accepts_both_arrow_directions() {
let mut selection = SetupSelection::default();
selection.move_cursor(true);
assert_eq!(selection.cursor, 1);
selection.move_cursor(true);
assert_eq!(selection.cursor, 2);
selection.move_cursor(false);
assert_eq!(selection.cursor, 1);
selection.move_cursor(false);
assert_eq!(selection.cursor, 0);
let backend = TestBackend::new(100, 30);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|frame| draw_setup_select(frame, &selection))
.unwrap();
let output = terminal
.backend()
.buffer()
.content
.iter()
.map(|cell| cell.symbol())
.collect::<String>();
assert!(output.contains("▶▶"));
assert!(!output.contains("SELECTED"));
assert!(output.contains("[ ]"));
assert!(output.contains("Claude"));
assert!(output.contains("Select with arrow keys"));
}
#[test]
fn mouse_click_toggles_setup_provider() {
let area = Rect::new(0, 0, 100, 30);
let mut selection = SetupSelection::default();
assert!(selection.providers().is_empty());
handle_setup_mouse(
&mut selection,
MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 5,
row: 10,
modifiers: KeyModifiers::NONE,
},
area,
);
assert_eq!(selection.cursor, 1);
assert!(selection.codex);
}
#[test]
fn mouse_selects_dashboard_rail_and_footer_action() {
let mut app = App::new(None, "loaded".into());
app.entries = vec![
NavEntry {
provider: "codex".into(),
email: "one@example.com".into(),
health: Health::Live,
snapshot: None,
error: None,
},
NavEntry {
provider: "claude".into(),
email: "two@example.com".into(),
health: Health::Live,
snapshot: None,
error: None,
},
];
let area = Rect::new(0, 0, 120, 30);
let click = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 5,
row: 7,
modifiers: KeyModifiers::NONE,
};
assert_eq!(
handle_dashboard_mouse(&mut app, click, area),
DashboardMouseAction::None
);
assert_eq!(app.selected, 1);
assert_eq!(
footer_action_at(&app, Rect::new(0, 28, 120, 2), 1, 29),
DashboardMouseAction::Refresh
);
}
#[test]
fn mouse_clicks_provider_heading_and_add_account_in_rail() {
let mut app = App::new(None, "loaded".into());
app.entries = vec![
NavEntry {
provider: "antigravity".into(),
email: "ag@example.com".into(),
health: Health::Live,
snapshot: None,
error: None,
},
NavEntry {
provider: "codex".into(),
email: "codex@example.com".into(),
health: Health::Live,
snapshot: None,
error: None,
},
];
let area = Rect::new(0, 0, 120, 30);
let codex_heading = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 5,
row: 6,
modifiers: KeyModifiers::NONE,
};
assert_eq!(
handle_dashboard_mouse(&mut app, codex_heading, area),
DashboardMouseAction::None
);
assert_eq!(app.selected, 1);
let add_account = MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column: 5,
row: 26,
modifiers: KeyModifiers::NONE,
};
assert_eq!(
handle_dashboard_mouse(&mut app, add_account, area),
DashboardMouseAction::Setup
);
}
#[test]
fn reads_custom_and_default_refresh_interval() {
let temp_dir = tempfile::tempdir().unwrap();
let default_config_path = temp_dir.path().join("cpa_default.json");
let custom_config_path = temp_dir.path().join("cpa_custom.json");
let default_stored = json!({
"managementKey": "secret123"
});
fs::write(&default_config_path, serde_json::to_vec(&default_stored).unwrap()).unwrap();
let custom_stored = json!({
"managementKey": "secret123",
"refreshInterval": 30
});
fs::write(&custom_config_path, serde_json::to_vec(&custom_stored).unwrap()).unwrap();
let default_cpa = read_cpa_config(&default_config_path).unwrap();
assert_eq!(default_cpa.refresh_interval, 60);
let custom_cpa = read_cpa_config(&custom_config_path).unwrap();
assert_eq!(custom_cpa.refresh_interval, 30);
}
}