mirror of
https://github.com/tiennm99/claude-code-usage-bubble.git
synced 2026-10-27 14:19:29 +00:00
feat: drop the OpenCode Go provider placeholder
OpenCode Go never had a usage source, so the provider list, its colours, label, window lengths, default position and settings fields go. With it goes the live/non-live split it introduced: every provider is now live, and toggling still keeps at least one enabled. Saved settings that carry the old OpenCode Go keys still load; the keys are dropped on next save.
This commit is contained in:
1 parent
ef54cc401c
commit
4299c8e602
15 files changed
+69
-216
No files matched your search
@@ -182,9 +182,6 @@ Use the right-click **Providers** menu to choose what is shown:
|
||||
|
||||
- **Claude Code** is enabled by default
|
||||
- **Codex** can be enabled alongside Claude Code or shown by itself
|
||||
- **OpenCode Go** is listed for future support; usage bars remain disabled
|
||||
until OpenCode exposes a stable quota source or console integration is
|
||||
explicitly enabled
|
||||
|
||||
When multiple providers are shown, each gets its own bubble that you can position
|
||||
independently.
|
||||
|
||||
+1
-3
@@ -126,8 +126,6 @@ fn base_color(kind: ProviderId) -> [u8; 3] {
|
||||
ProviderId::Claude => [0x2a, 0x1f, 0x1c],
|
||||
// Cool dark slate for Codex.
|
||||
ProviderId::ChatGpt => [0x1a, 0x1f, 0x26],
|
||||
// Deep green for OpenCode Go.
|
||||
ProviderId::OpenCodeGo => [0x12, 0x2a, 0x20],
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +176,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn arc_starts_at_twelve_o_clock_for_any_percent() {
|
||||
for kind in ProviderId::LIVE_USAGE {
|
||||
for kind in ProviderId::ALL {
|
||||
for pct in [0.5, 1.0, 25.0, 59.9, 60.0, 80.0, 95.0, 100.0, 150.0] {
|
||||
let pixmap = render_pixmap(kind, Some(pct));
|
||||
assert_eq!(
|
||||
|
||||
+1
-2
@@ -263,7 +263,7 @@ fn set_size(window: &WebviewWindow, (w, h): (i32, i32)) {
|
||||
|
||||
/// Bubble windows that exist, with their provider, in display order.
|
||||
fn windows(app: &AppHandle) -> Vec<(ProviderId, WebviewWindow)> {
|
||||
ProviderId::LIVE_USAGE
|
||||
ProviderId::ALL
|
||||
.into_iter()
|
||||
.filter_map(|p| {
|
||||
app.get_webview_window(&runtime::bubble_label(p))
|
||||
@@ -438,7 +438,6 @@ pub fn mark_ready(app: &AppHandle, label: &str) -> Result<(), String> {
|
||||
let provider = label
|
||||
.strip_prefix(BUBBLE_LABEL_PREFIX)
|
||||
.and_then(ProviderId::from_slug)
|
||||
.filter(|p| p.metadata().live_usage)
|
||||
.ok_or_else(|| "not a bubble window".to_string())?;
|
||||
if app.get_webview_window(label).is_none() {
|
||||
return Err("no such bubble window".into());
|
||||
|
||||
+11
-13
@@ -24,7 +24,7 @@ pub fn get_bubble_state(
|
||||
runtime: State<'_, Arc<Runtime>>,
|
||||
provider: String,
|
||||
) -> Result<BubblePayload, String> {
|
||||
let provider = live_provider(&provider)?;
|
||||
let provider = parse_provider(&provider)?;
|
||||
let scale = window.scale_factor().map_err(|err| err.to_string())?;
|
||||
Ok(runtime.bubble_payload(provider, scale))
|
||||
}
|
||||
@@ -66,7 +66,7 @@ pub fn bubble_ready(app: AppHandle, label: String) -> Result<(), String> {
|
||||
/// running inside the webview's IPC callback.
|
||||
#[tauri::command]
|
||||
pub async fn bubble_click(runtime: State<'_, Arc<Runtime>>, slug: String) -> Result<(), String> {
|
||||
let provider = live_provider(&slug)?;
|
||||
let provider = parse_provider(&slug)?;
|
||||
runtime.bubble_clicked(provider);
|
||||
Ok(())
|
||||
}
|
||||
@@ -74,7 +74,7 @@ pub async fn bubble_click(runtime: State<'_, Arc<Runtime>>, slug: String) -> Res
|
||||
/// The page is about to start an OS drag of a bubble.
|
||||
#[tauri::command]
|
||||
pub fn bubble_drag_start(app: AppHandle, slug: String) -> Result<(), String> {
|
||||
let provider = live_provider(&slug)?;
|
||||
let provider = parse_provider(&slug)?;
|
||||
bubble::drag_start(&app, provider);
|
||||
Ok(())
|
||||
}
|
||||
@@ -96,10 +96,8 @@ fn resize_delta(dir: i32) -> Result<i32, String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn live_provider(slug: &str) -> Result<ProviderId, String> {
|
||||
ProviderId::from_slug(slug)
|
||||
.filter(|p| p.metadata().live_usage)
|
||||
.ok_or_else(|| "unknown provider".to_string())
|
||||
fn parse_provider(slug: &str) -> Result<ProviderId, String> {
|
||||
ProviderId::from_slug(slug).ok_or_else(|| "unknown provider".to_string())
|
||||
}
|
||||
|
||||
/// One log line: control characters become spaces, so a message cannot
|
||||
@@ -117,12 +115,12 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn only_live_providers_are_accepted() {
|
||||
assert_eq!(live_provider("claude"), Ok(ProviderId::Claude));
|
||||
assert_eq!(live_provider("chatgpt"), Ok(ProviderId::ChatGpt));
|
||||
assert!(live_provider("opencode-go").is_err());
|
||||
assert!(live_provider("").is_err());
|
||||
assert!(live_provider("CLAUDE").is_err());
|
||||
fn only_known_providers_are_accepted() {
|
||||
assert_eq!(parse_provider("claude"), Ok(ProviderId::Claude));
|
||||
assert_eq!(parse_provider("chatgpt"), Ok(ProviderId::ChatGpt));
|
||||
assert!(parse_provider("codex").is_err());
|
||||
assert!(parse_provider("").is_err());
|
||||
assert!(parse_provider("CLAUDE").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+1
-6
@@ -163,7 +163,6 @@ pub fn default_position(
|
||||
let stagger = match model {
|
||||
ProviderId::Claude => 0,
|
||||
ProviderId::ChatGpt => height_px + STAGGER_GAP,
|
||||
ProviderId::OpenCodeGo => 2 * (height_px + STAGGER_GAP),
|
||||
};
|
||||
let x = work_area.r - width_px - STAGGER_GAP;
|
||||
let y = work_area.b - height_px - STAGGER_GAP - stagger;
|
||||
@@ -210,7 +209,7 @@ pub struct SnapInput {
|
||||
pub dpi: u32,
|
||||
/// Docked taskbar edge and rect, when the platform reports one.
|
||||
pub taskbar: Option<(Edge, Rect)>,
|
||||
/// Outer rects of the other bubbles, in `ProviderId::LIVE_USAGE` order.
|
||||
/// Outer rects of the other bubbles, in `ProviderId::ALL` order.
|
||||
pub peers: Vec<Rect>,
|
||||
}
|
||||
|
||||
@@ -499,10 +498,6 @@ mod tests {
|
||||
default_position(WA, w, h, ProviderId::ChatGpt),
|
||||
Point::new(1696, 1040 - 66 - 24 - 90)
|
||||
);
|
||||
assert_eq!(
|
||||
default_position(WA, w, h, ProviderId::OpenCodeGo),
|
||||
Point::new(1696, 1040 - 66 - 24 - 180)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -24,7 +24,6 @@ pub struct LocaleStrings {
|
||||
pub providers: String,
|
||||
pub claude_label: String,
|
||||
pub chatgpt_label: String,
|
||||
pub opencode_go_label: String,
|
||||
pub settings: String,
|
||||
pub start_with_windows: String,
|
||||
pub start_at_login: String,
|
||||
@@ -124,7 +123,6 @@ impl LocaleStrings {
|
||||
match provider {
|
||||
ProviderId::Claude => &self.claude_label,
|
||||
ProviderId::ChatGpt => &self.chatgpt_label,
|
||||
ProviderId::OpenCodeGo => &self.opencode_go_label,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,7 +139,6 @@ fn english_strings() -> LocaleStrings {
|
||||
providers: "Providers".into(),
|
||||
claude_label: "Claude Code".into(),
|
||||
chatgpt_label: "Codex".into(),
|
||||
opencode_go_label: "OpenCode Go".into(),
|
||||
settings: "Settings".into(),
|
||||
start_with_windows: "Start with Windows".into(),
|
||||
start_at_login: "Start at login".into(),
|
||||
|
||||
@@ -205,7 +205,7 @@ pub fn build(input: MenuInput<'_>) -> Vec<MenuEntry> {
|
||||
MenuCommand::ToggleProvider(p),
|
||||
s.provider_label(p),
|
||||
settings.is_provider_enabled(p),
|
||||
p.metadata().live_usage,
|
||||
true,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
@@ -360,7 +360,6 @@ mod tests {
|
||||
poll_interval_ms: POLL_15_MIN,
|
||||
update_check_interval_secs: Some(UPDATE_CHECK_DAILY_SECS),
|
||||
show_codex: true,
|
||||
show_opencode_go: true,
|
||||
widget_visible: false,
|
||||
..Settings::default()
|
||||
};
|
||||
@@ -371,9 +370,6 @@ mod tests {
|
||||
assert!(!checked(&m, "update-interval:off"));
|
||||
assert!(checked(&m, "provider:claude"));
|
||||
assert!(checked(&m, "provider:chatgpt"));
|
||||
// OpenCode Go mirrors its flag but stays greyed.
|
||||
assert!(checked(&m, "provider:opencode-go"));
|
||||
assert!(!enabled(&m, "provider:opencode-go"));
|
||||
assert!(checked(&m, "autostart"));
|
||||
assert!(!checked(&m, "toggle-widget"));
|
||||
}
|
||||
@@ -524,7 +520,6 @@ mod tests {
|
||||
"Providers >",
|
||||
" Claude Code [provider:claude] on",
|
||||
" Codex [provider:chatgpt] off",
|
||||
" OpenCode Go [provider:opencode-go] off grey",
|
||||
"Settings >",
|
||||
&autostart,
|
||||
" Reset position [reset-position]",
|
||||
@@ -572,7 +567,7 @@ mod tests {
|
||||
}
|
||||
let mut commands = Vec::new();
|
||||
walk(&m, &mut commands);
|
||||
assert_eq!(commands.len(), 21);
|
||||
assert_eq!(commands.len(), 20);
|
||||
for c in commands {
|
||||
assert_eq!(MenuCommand::parse(&c.id()), Some(c), "{}", c.id());
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ use crate::usage::ProviderId;
|
||||
|
||||
const FIVE_HOURS_SECS: u64 = 5 * 60 * 60;
|
||||
const SEVEN_DAYS_SECS: u64 = 7 * 24 * 60 * 60;
|
||||
const THIRTY_DAYS_SECS: u64 = 30 * 24 * 60 * 60;
|
||||
|
||||
/// Placeholder for the head label and weekly countdown before the first poll.
|
||||
const PENDING: &str = "…";
|
||||
@@ -25,7 +24,6 @@ const PENDING: &str = "…";
|
||||
pub fn window_durations_secs(provider: ProviderId) -> (u64, u64) {
|
||||
match provider {
|
||||
ProviderId::Claude | ProviderId::ChatGpt => (FIVE_HOURS_SECS, SEVEN_DAYS_SECS),
|
||||
ProviderId::OpenCodeGo => (SEVEN_DAYS_SECS, THIRTY_DAYS_SECS),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,12 +337,4 @@ mod tests {
|
||||
assert!(panel.get(key).is_some(), "missing {key}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opencode_go_uses_weekly_and_monthly_windows() {
|
||||
assert_eq!(
|
||||
window_durations_secs(ProviderId::OpenCodeGo),
|
||||
(604_800, 2_592_000)
|
||||
);
|
||||
}
|
||||
}
|
||||
+34
-68
@@ -40,9 +40,6 @@ fn default_show_claude() -> bool {
|
||||
fn default_show_codex() -> bool {
|
||||
false
|
||||
}
|
||||
fn default_show_opencode_go() -> bool {
|
||||
false
|
||||
}
|
||||
fn default_widget_visible() -> bool {
|
||||
true
|
||||
}
|
||||
@@ -60,7 +57,6 @@ fn default_update_check_interval_secs() -> Option<u64> {
|
||||
pub struct BubblePositions {
|
||||
pub claude: Option<(i32, i32)>,
|
||||
pub codex: Option<(i32, i32)>,
|
||||
pub opencode_go: Option<(i32, i32)>,
|
||||
}
|
||||
|
||||
impl BubblePositions {
|
||||
@@ -68,27 +64,23 @@ impl BubblePositions {
|
||||
match provider {
|
||||
ProviderId::Claude => self.claude,
|
||||
ProviderId::ChatGpt => self.codex,
|
||||
ProviderId::OpenCodeGo => self.opencode_go,
|
||||
}
|
||||
}
|
||||
pub fn set(&mut self, provider: ProviderId, pos: (i32, i32)) {
|
||||
match provider {
|
||||
ProviderId::Claude => self.claude = Some(pos),
|
||||
ProviderId::ChatGpt => self.codex = Some(pos),
|
||||
ProviderId::OpenCodeGo => self.opencode_go = Some(pos),
|
||||
}
|
||||
}
|
||||
pub fn reset(&mut self, provider: ProviderId) {
|
||||
match provider {
|
||||
ProviderId::Claude => self.claude = None,
|
||||
ProviderId::ChatGpt => self.codex = None,
|
||||
ProviderId::OpenCodeGo => self.opencode_go = None,
|
||||
}
|
||||
}
|
||||
pub fn reset_all(&mut self) {
|
||||
self.claude = None;
|
||||
self.codex = None;
|
||||
self.opencode_go = None;
|
||||
}
|
||||
|
||||
/// Drop any saved position whose top-left no longer falls on one of
|
||||
@@ -115,8 +107,6 @@ pub struct Settings {
|
||||
pub show_claude_code: bool,
|
||||
#[serde(default = "default_show_codex")]
|
||||
pub show_codex: bool,
|
||||
#[serde(default = "default_show_opencode_go")]
|
||||
pub show_opencode_go: bool,
|
||||
#[serde(default)]
|
||||
pub bubble_positions: BubblePositions,
|
||||
#[serde(default = "default_bubble_size")]
|
||||
@@ -136,7 +126,6 @@ impl Default for Settings {
|
||||
Self {
|
||||
show_claude_code: default_show_claude(),
|
||||
show_codex: default_show_codex(),
|
||||
show_opencode_go: default_show_opencode_go(),
|
||||
bubble_positions: BubblePositions::default(),
|
||||
bubble_size_logical: default_bubble_size(),
|
||||
poll_interval_ms: default_poll_interval_ms(),
|
||||
@@ -152,7 +141,6 @@ impl Settings {
|
||||
match provider {
|
||||
ProviderId::Claude => self.show_claude_code,
|
||||
ProviderId::ChatGpt => self.show_codex,
|
||||
ProviderId::OpenCodeGo => self.show_opencode_go,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,30 +148,23 @@ impl Settings {
|
||||
match provider {
|
||||
ProviderId::Claude => self.show_claude_code = enabled,
|
||||
ProviderId::ChatGpt => self.show_codex = enabled,
|
||||
ProviderId::OpenCodeGo => self.show_opencode_go = enabled,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_provider(&mut self, provider: ProviderId) {
|
||||
/// Menu toggle for a provider. Unchecking the last enabled provider is
|
||||
/// undone so the app always shows something. Returns the provider's new
|
||||
/// state.
|
||||
pub fn toggle_provider(&mut self, provider: ProviderId) -> bool {
|
||||
let enabled = !self.is_provider_enabled(provider);
|
||||
self.set_provider_enabled(provider, enabled);
|
||||
}
|
||||
|
||||
/// Menu toggle for a provider. Providers without live usage cannot be
|
||||
/// toggled, and unchecking the last enabled live provider is undone so
|
||||
/// the app always shows something. Returns the provider's new state.
|
||||
pub fn toggle_live_provider(&mut self, provider: ProviderId) -> bool {
|
||||
if provider.metadata().live_usage {
|
||||
self.toggle_provider(provider);
|
||||
if !self.has_enabled_live_provider() {
|
||||
self.set_provider_enabled(provider, true);
|
||||
}
|
||||
if !self.has_enabled_provider() {
|
||||
self.set_provider_enabled(provider, true);
|
||||
}
|
||||
self.is_provider_enabled(provider)
|
||||
}
|
||||
|
||||
pub fn has_enabled_live_provider(&self) -> bool {
|
||||
ProviderId::LIVE_USAGE
|
||||
pub fn has_enabled_provider(&self) -> bool {
|
||||
ProviderId::ALL
|
||||
.iter()
|
||||
.any(|provider| self.is_provider_enabled(*provider))
|
||||
}
|
||||
@@ -310,8 +291,8 @@ fn parse(content: &str) -> Option<Settings> {
|
||||
|
||||
/// Enforce the invariants a hand-edited file may break.
|
||||
fn normalise(mut settings: Settings) -> Settings {
|
||||
// At least one live provider must be visible. Otherwise the app has nothing to show.
|
||||
if !settings.has_enabled_live_provider() {
|
||||
// At least one provider must be visible. Otherwise the app has nothing to show.
|
||||
if !settings.has_enabled_provider() {
|
||||
settings.show_claude_code = true;
|
||||
}
|
||||
// Clamp bubble size to safe range in case settings.json was hand-edited.
|
||||
@@ -408,7 +389,6 @@ mod tests {
|
||||
|
||||
assert!(!settings.show_claude_code);
|
||||
assert!(settings.show_codex);
|
||||
assert!(!settings.show_opencode_go);
|
||||
assert_eq!(
|
||||
settings.bubble_positions.get(ProviderId::Claude),
|
||||
Some((10, 20))
|
||||
@@ -417,30 +397,32 @@ mod tests {
|
||||
settings.bubble_positions.get(ProviderId::ChatGpt),
|
||||
Some((30, 40))
|
||||
);
|
||||
assert_eq!(settings.bubble_positions.get(ProviderId::OpenCodeGo), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn three_provider_settings_json_loads() {
|
||||
fn retired_opencode_go_keys_are_ignored() {
|
||||
let json = r#"{
|
||||
"show_claude_code": true,
|
||||
"show_codex": false,
|
||||
"show_claude_code": false,
|
||||
"show_codex": true,
|
||||
"show_opencode_go": true,
|
||||
"bubble_positions": {
|
||||
"codex": [30, 40],
|
||||
"opencode_go": [50, 60]
|
||||
}
|
||||
}"#;
|
||||
|
||||
let settings: Settings =
|
||||
serde_json::from_str(json).expect("new settings json should parse");
|
||||
serde_json::from_str(json).expect("settings with retired keys should parse");
|
||||
|
||||
assert!(settings.show_claude_code);
|
||||
assert!(!settings.show_codex);
|
||||
assert!(settings.show_opencode_go);
|
||||
assert!(!settings.show_claude_code);
|
||||
assert!(settings.show_codex);
|
||||
assert_eq!(
|
||||
settings.bubble_positions.get(ProviderId::OpenCodeGo),
|
||||
Some((50, 60))
|
||||
settings.bubble_positions.get(ProviderId::ChatGpt),
|
||||
Some((30, 40))
|
||||
);
|
||||
let keys = serde_json::to_value(&settings).expect("serialise");
|
||||
assert!(keys.get("show_opencode_go").is_none());
|
||||
assert!(keys["bubble_positions"].get("opencode_go").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -518,10 +500,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalise_restores_a_live_provider_and_clamps_size() {
|
||||
fn normalise_restores_a_provider_and_clamps_size() {
|
||||
let settings = parse_and_normalise(
|
||||
r#"{"show_claude_code": false, "show_codex": false, "show_opencode_go": true,
|
||||
"bubble_size_logical": 9000}"#,
|
||||
r#"{"show_claude_code": false, "show_codex": false, "bubble_size_logical": 9000}"#,
|
||||
);
|
||||
assert!(settings.show_claude_code);
|
||||
assert_eq!(settings.bubble_size_logical, MAX_BUBBLE_SIZE);
|
||||
@@ -543,7 +524,6 @@ mod tests {
|
||||
"poll_interval_ms",
|
||||
"show_claude_code",
|
||||
"show_codex",
|
||||
"show_opencode_go",
|
||||
"update_check_interval_secs",
|
||||
"widget_visible",
|
||||
]
|
||||
@@ -557,15 +537,13 @@ mod tests {
|
||||
#[test]
|
||||
fn offscreen_positions_are_dropped_against_monitors() {
|
||||
let mut settings = parse_and_normalise(
|
||||
r#"{"bubble_positions": {"claude": [100, 100], "codex": [-3000, 100],
|
||||
"opencode_go": [2000, 500]}}"#,
|
||||
r#"{"bubble_positions": {"claude": [100, 100], "codex": [-3000, 100]}}"#,
|
||||
);
|
||||
let monitors = [Rect::new(0, 0, 1920, 1080), Rect::new(1920, 0, 3840, 1080)];
|
||||
drop_offscreen_positions(&mut settings, &monitors);
|
||||
let p = &settings.bubble_positions;
|
||||
assert_eq!(p.get(ProviderId::Claude), Some((100, 100)));
|
||||
assert_eq!(p.get(ProviderId::ChatGpt), None);
|
||||
assert_eq!(p.get(ProviderId::OpenCodeGo), Some((2000, 500)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -617,27 +595,15 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toggle_keeps_provider_flags_independent() {
|
||||
fn toggle_never_disables_the_last_provider() {
|
||||
let mut settings = Settings::default();
|
||||
settings.toggle_provider(ProviderId::ChatGpt);
|
||||
assert!(settings.is_provider_enabled(ProviderId::ChatGpt));
|
||||
settings.toggle_provider(ProviderId::Claude);
|
||||
assert!(!settings.is_provider_enabled(ProviderId::Claude));
|
||||
assert!(settings.has_enabled_live_provider());
|
||||
settings.toggle_provider(ProviderId::ChatGpt);
|
||||
assert!(!settings.has_enabled_live_provider());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn live_toggle_never_disables_the_last_live_provider() {
|
||||
let mut settings = Settings::default();
|
||||
assert!(settings.toggle_live_provider(ProviderId::Claude));
|
||||
assert!(settings.show_claude_code);
|
||||
assert!(settings.toggle_live_provider(ProviderId::ChatGpt));
|
||||
assert!(!settings.toggle_live_provider(ProviderId::Claude));
|
||||
assert!(settings.toggle_live_provider(ProviderId::ChatGpt));
|
||||
// OpenCode Go has no live usage, so its flag never changes.
|
||||
assert!(!settings.toggle_live_provider(ProviderId::OpenCodeGo));
|
||||
assert!(!settings.show_opencode_go);
|
||||
assert!(settings.toggle_provider(ProviderId::ChatGpt));
|
||||
assert!(!settings.toggle_provider(ProviderId::Claude));
|
||||
assert!(settings.has_enabled_provider());
|
||||
// ChatGPT is now the only enabled provider, so it stays on.
|
||||
assert!(settings.toggle_provider(ProviderId::ChatGpt));
|
||||
assert!(settings.show_codex);
|
||||
assert!(settings.toggle_provider(ProviderId::Claude));
|
||||
assert!(!settings.toggle_provider(ProviderId::ChatGpt));
|
||||
}
|
||||
}
|
||||
@@ -79,10 +79,6 @@ impl Alert {
|
||||
strings.chatgpt_token_expired_title.clone(),
|
||||
strings.chatgpt_token_expired_body.clone(),
|
||||
),
|
||||
ProviderId::OpenCodeGo => (
|
||||
strings.opencode_go_label.clone(),
|
||||
strings.update_failed.clone(),
|
||||
),
|
||||
},
|
||||
Alert::UpdateApplied { version } => (
|
||||
strings.update_applied_title.clone(),
|
||||
|
||||
@@ -19,7 +19,6 @@ pub fn accent_color_for(model: ProviderId, is_dark: bool) -> Color {
|
||||
Color::from_hex("#1A1A1A")
|
||||
}
|
||||
}
|
||||
ProviderId::OpenCodeGo => Color::from_hex("#3BAE75"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,7 +73,6 @@ mod tests {
|
||||
fn safe_band_uses_theme_aware_accent() {
|
||||
assert_eq!(hex(ProviderId::ChatGpt, true, 10.0), "#E5E5E5");
|
||||
assert_eq!(hex(ProviderId::ChatGpt, false, 10.0), "#1A1A1A");
|
||||
assert_eq!(hex(ProviderId::OpenCodeGo, false, 0.0), "#3BAE75");
|
||||
assert_eq!(
|
||||
accent_color_for(ProviderId::Claude, false),
|
||||
accent_color_for(ProviderId::Claude, true)
|
||||
|
||||
+11
-15
@@ -139,7 +139,7 @@ struct BubbleSnapshot {
|
||||
impl BubbleSnapshot {
|
||||
fn of(state: &AppState) -> Self {
|
||||
Self {
|
||||
providers: enabled_live_providers(&state.settings),
|
||||
providers: enabled_providers(&state.settings),
|
||||
usage: state.usage.clone(),
|
||||
strings: state.strings.clone(),
|
||||
is_dark: state.is_dark,
|
||||
@@ -264,7 +264,7 @@ impl Runtime {
|
||||
};
|
||||
let (views, menu_snapshot) = runtime.with_state(|s| {
|
||||
(
|
||||
tray::views(&enabled_live_providers(&s.settings), &s.usage, &s.strings),
|
||||
tray::views(&enabled_providers(&s.settings), &s.usage, &s.strings),
|
||||
MenuSnapshot::of(s),
|
||||
)
|
||||
});
|
||||
@@ -468,13 +468,10 @@ impl Runtime {
|
||||
});
|
||||
}
|
||||
|
||||
/// Toggle a live provider, keeping at least one enabled, then create or
|
||||
/// Toggle a provider, keeping at least one enabled, then create or
|
||||
/// destroy its bubble, update the tray and poll.
|
||||
fn toggle_provider(self: &Arc<Self>, provider: ProviderId) {
|
||||
if !provider.metadata().live_usage {
|
||||
return;
|
||||
}
|
||||
let enabled = self.update_settings(|s| s.toggle_live_provider(provider));
|
||||
let enabled = self.update_settings(|s| s.toggle_provider(provider));
|
||||
if enabled {
|
||||
bubble::spawn_create(&self.ui.app, provider);
|
||||
} else if let Some(window) = self.ui.app.get_webview_window(&bubble_label(provider)) {
|
||||
@@ -835,9 +832,9 @@ pub fn bubble_label(provider: ProviderId) -> String {
|
||||
format!("{BUBBLE_LABEL_PREFIX}{}", provider.slug())
|
||||
}
|
||||
|
||||
/// Live providers the user has enabled, in display order.
|
||||
pub fn enabled_live_providers(settings: &Settings) -> Vec<ProviderId> {
|
||||
ProviderId::LIVE_USAGE
|
||||
/// Providers the user has enabled, in display order.
|
||||
pub fn enabled_providers(settings: &Settings) -> Vec<ProviderId> {
|
||||
ProviderId::ALL
|
||||
.into_iter()
|
||||
.filter(|p| settings.is_provider_enabled(*p))
|
||||
.collect()
|
||||
@@ -1008,7 +1005,7 @@ fn setup(app: &mut tauri::App, cli: &CliArgs) -> Result<(), Box<dyn Error>> {
|
||||
update_interval_secs: state.settings.update_check_interval_secs,
|
||||
last_update_check_unix: state.settings.last_update_check_unix,
|
||||
};
|
||||
let providers = enabled_live_providers(&state.settings);
|
||||
let providers = enabled_providers(&state.settings);
|
||||
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let runtime = Arc::new(Runtime {
|
||||
@@ -1150,18 +1147,17 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enabled_providers_keep_display_order_and_skip_non_live_ones() {
|
||||
fn enabled_providers_keep_display_order() {
|
||||
let mut settings = Settings {
|
||||
show_codex: true,
|
||||
show_opencode_go: true,
|
||||
..Settings::default()
|
||||
};
|
||||
assert_eq!(
|
||||
enabled_live_providers(&settings),
|
||||
enabled_providers(&settings),
|
||||
[ProviderId::Claude, ProviderId::ChatGpt]
|
||||
);
|
||||
settings.show_claude_code = false;
|
||||
assert_eq!(enabled_live_providers(&settings), [ProviderId::ChatGpt]);
|
||||
assert_eq!(enabled_providers(&settings), [ProviderId::ChatGpt]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+5
-11
@@ -37,7 +37,6 @@ pub struct TrayView {
|
||||
pub fn views(enabled: &[ProviderId], usage: &UsageState, strings: &LocaleStrings) -> Vec<TrayView> {
|
||||
enabled
|
||||
.iter()
|
||||
.filter(|p| p.metadata().live_usage)
|
||||
.map(|&provider| {
|
||||
let entry = usage.snapshots.get(&provider);
|
||||
TrayView {
|
||||
@@ -106,7 +105,7 @@ struct Plan {
|
||||
|
||||
fn plan(shown: &HashMap<ProviderId, Shown>, views: &[TrayView]) -> Plan {
|
||||
let mut plan = Plan {
|
||||
remove: ProviderId::LIVE_USAGE
|
||||
remove: ProviderId::ALL
|
||||
.into_iter()
|
||||
.filter(|p| shown.contains_key(p) && !views.iter().any(|v| v.provider == *p))
|
||||
.collect(),
|
||||
@@ -153,7 +152,7 @@ impl Trays {
|
||||
/// The providers that currently have an icon.
|
||||
pub fn providers(&self) -> Vec<ProviderId> {
|
||||
let shown = self.shown();
|
||||
ProviderId::LIVE_USAGE
|
||||
ProviderId::ALL
|
||||
.into_iter()
|
||||
.filter(|p| shown.contains_key(p))
|
||||
.collect()
|
||||
@@ -319,13 +318,8 @@ mod tests {
|
||||
#[test]
|
||||
fn views_follow_enabled_providers_and_last_poll() {
|
||||
let s = strings();
|
||||
let all = [
|
||||
ProviderId::Claude,
|
||||
ProviderId::ChatGpt,
|
||||
ProviderId::OpenCodeGo,
|
||||
];
|
||||
let v = views(&all, &usage(42.0, true), &s);
|
||||
assert_eq!(v.len(), 2, "OpenCode Go never gets an icon");
|
||||
let v = views(&ProviderId::ALL, &usage(42.0, true), &s);
|
||||
assert_eq!(v.len(), 2);
|
||||
assert_eq!(v[0].provider, ProviderId::Claude);
|
||||
assert_eq!(v[0].percent, Some(42.0));
|
||||
assert_eq!(
|
||||
@@ -335,7 +329,7 @@ mod tests {
|
||||
assert_eq!(v[1].provider, ProviderId::ChatGpt);
|
||||
assert_eq!(v[1].percent, None);
|
||||
// A failed last poll greys every badge to the track.
|
||||
let v = views(&all, &usage(42.0, false), &s);
|
||||
let v = views(&ProviderId::ALL, &usage(42.0, false), &s);
|
||||
assert_eq!(v[0].percent, None);
|
||||
assert!(views(&[ProviderId::ChatGpt], &usage(1.0, true), &s)
|
||||
.iter()
|
||||
|
||||
+2
-18
@@ -31,16 +31,13 @@ impl Registry {
|
||||
settings: &Settings,
|
||||
) -> Vec<(ProviderId, Result<UsageWindows, Error>)> {
|
||||
let mut out = Vec::new();
|
||||
for provider in ProviderId::LIVE_USAGE {
|
||||
for provider in ProviderId::ALL {
|
||||
if !settings.is_provider_enabled(provider) {
|
||||
continue;
|
||||
}
|
||||
let result = match provider {
|
||||
ProviderId::Claude => self.claude.poll(http),
|
||||
ProviderId::ChatGpt => self.chatgpt.poll(http),
|
||||
ProviderId::OpenCodeGo => {
|
||||
unreachable!("OpenCode Go has no live usage provider yet")
|
||||
}
|
||||
};
|
||||
out.push((provider, result));
|
||||
}
|
||||
@@ -56,7 +53,6 @@ impl Registry {
|
||||
let locator = match id {
|
||||
ProviderId::Claude => self.claude.locator(),
|
||||
ProviderId::ChatGpt => self.chatgpt.locator(),
|
||||
ProviderId::OpenCodeGo => return refresh::Outcome::CliMissing,
|
||||
};
|
||||
match locator.first_available() {
|
||||
Some(src) => orchestrator.refresh(src),
|
||||
@@ -80,18 +76,6 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::net::USER_AGENT;
|
||||
|
||||
#[test]
|
||||
fn live_usage_excludes_opencode_go() {
|
||||
// `poll_enabled` relies on this for its `unreachable!` arm.
|
||||
assert!(!ProviderId::LIVE_USAGE.contains(&ProviderId::OpenCodeGo));
|
||||
for provider in ProviderId::ALL {
|
||||
assert_eq!(
|
||||
ProviderId::LIVE_USAGE.contains(&provider),
|
||||
provider.metadata().live_usage
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Polls the real providers with the credentials on this machine. Run by
|
||||
/// hand: `cargo test -- --ignored live_poll --nocapture`.
|
||||
#[test]
|
||||
@@ -104,7 +88,7 @@ mod tests {
|
||||
}
|
||||
let mut registry = Registry::with_defaults();
|
||||
let results = registry.poll_enabled(&http, &settings);
|
||||
assert_eq!(results.len(), ProviderId::LIVE_USAGE.len());
|
||||
assert_eq!(results.len(), ProviderId::ALL.len());
|
||||
for (provider, result) in results {
|
||||
match result {
|
||||
Ok(w) => println!(
|
||||
|
||||
+1
-51
@@ -12,19 +12,15 @@ pub enum ProviderId {
|
||||
Claude,
|
||||
#[serde(rename = "chatgpt")]
|
||||
ChatGpt,
|
||||
#[serde(rename = "opencode-go")]
|
||||
OpenCodeGo,
|
||||
}
|
||||
|
||||
impl ProviderId {
|
||||
pub const ALL: [Self; 3] = [Self::Claude, Self::ChatGpt, Self::OpenCodeGo];
|
||||
pub const LIVE_USAGE: [Self; 2] = [Self::Claude, Self::ChatGpt];
|
||||
pub const ALL: [Self; 2] = [Self::Claude, Self::ChatGpt];
|
||||
|
||||
pub fn slug(self) -> &'static str {
|
||||
match self {
|
||||
Self::Claude => "claude",
|
||||
Self::ChatGpt => "chatgpt",
|
||||
Self::OpenCodeGo => "opencode-go",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,51 +28,6 @@ impl ProviderId {
|
||||
pub fn from_slug(slug: &str) -> Option<Self> {
|
||||
Self::ALL.into_iter().find(|p| p.slug() == slug)
|
||||
}
|
||||
|
||||
pub fn metadata(self) -> ProviderMetadata {
|
||||
match self {
|
||||
Self::Claude => ProviderMetadata {
|
||||
id: self,
|
||||
slug: "claude",
|
||||
default_enabled: true,
|
||||
tray_icon_id: 1,
|
||||
display_mode: ProviderDisplayMode::TwoWindow,
|
||||
live_usage: true,
|
||||
},
|
||||
Self::ChatGpt => ProviderMetadata {
|
||||
id: self,
|
||||
slug: "chatgpt",
|
||||
default_enabled: false,
|
||||
tray_icon_id: 2,
|
||||
display_mode: ProviderDisplayMode::TwoWindow,
|
||||
live_usage: true,
|
||||
},
|
||||
Self::OpenCodeGo => ProviderMetadata {
|
||||
id: self,
|
||||
slug: "opencode-go",
|
||||
default_enabled: false,
|
||||
tray_icon_id: 3,
|
||||
display_mode: ProviderDisplayMode::WeeklyMonthlyFourBar,
|
||||
live_usage: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum ProviderDisplayMode {
|
||||
TwoWindow,
|
||||
WeeklyMonthlyFourBar,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct ProviderMetadata {
|
||||
pub id: ProviderId,
|
||||
pub slug: &'static str,
|
||||
pub default_enabled: bool,
|
||||
pub tray_icon_id: u32,
|
||||
pub display_mode: ProviderDisplayMode,
|
||||
pub live_usage: bool,
|
||||
}
|
||||
|
||||
/// One usage window: how much you've consumed (0–100), and when it resets.
|
||||
@@ -110,7 +61,6 @@ mod tests {
|
||||
let json = serde_json::to_string(&provider).expect("serialise");
|
||||
assert_eq!(json, format!("\"{}\"", provider.slug()));
|
||||
assert_eq!(ProviderId::from_slug(provider.slug()), Some(provider));
|
||||
assert_eq!(provider.metadata().slug, provider.slug());
|
||||
}
|
||||
assert_eq!(ProviderId::from_slug("codex"), None);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user