use serde_json::Value;
use super::strip::{StripContent, StripMetric, quota_group};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum StatusItemContent {
AppIcon,
Chart,
Logos,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum MenuBarLook {
Chart,
Logos,
Name,
}
impl MenuBarLook {
pub(super) fn from_style(style: Option<&str>) -> Self {
match style {
Some("provider") => Self::Logos,
Some("name") => Self::Name,
_ => Self::Chart,
}
}
pub(super) fn style(self) -> &'static str {
match self {
Self::Chart => "bars",
Self::Logos => "provider",
Self::Name => "name",
}
}
pub(super) fn picker_value(self) -> &'static str {
match self {
Self::Chart => "chart",
Self::Logos => "logos",
Self::Name => "name",
}
}
pub(super) fn from_picker_value(value: &str) -> Option<Self> {
match value {
"chart" => Some(Self::Chart),
"logos" => Some(Self::Logos),
"name" => Some(Self::Name),
_ => None,
}
}
}
pub(super) fn status_item_content(look: MenuBarLook, has_content: bool) -> StatusItemContent {
if !has_content {
StatusItemContent::AppIcon
} else if look == MenuBarLook::Chart {
StatusItemContent::Chart
} else {
StatusItemContent::Logos
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum UsageReading {
Used,
Left,
}
impl UsageReading {
pub(super) fn from_strip_ipc(value: &Value) -> Self {
match value.get("show_as").and_then(Value::as_str) {
Some("left") => Self::Left,
_ => Self::Used,
}
}
}
fn metric_text(metric: &StripMetric, reading: UsageReading) -> &str {
match (reading, metric.left_value.as_deref()) {
(UsageReading::Left, Some(left)) => left,
_ => metric.value.trim(),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct FallbackItem {
pub(super) id: &'static str,
pub(super) label: &'static str,
}
pub(super) fn fallback_menu_items() -> [FallbackItem; 2] {
[
FallbackItem {
id: "fallback-refresh",
label: "Refresh",
},
FallbackItem {
id: "fallback-quit",
label: "Quit AI Usage",
},
]
}
pub(super) fn fallback_menu_attached(webview_built: bool) -> bool {
!webview_built
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct LogoSegment {
pub(super) slug: String,
pub(super) short_name: Option<String>,
pub(super) with_name: bool,
pub(super) values: Vec<String>,
}
pub(super) fn logo_segments(
content: &StripContent,
report: &Value,
look: MenuBarLook,
selected: Option<&str>,
show_short_name: bool,
reading: UsageReading,
) -> Vec<LogoSegment> {
if look == MenuBarLook::Name {
return name_segment(content, report, selected, show_short_name, reading)
.into_iter()
.collect();
}
content
.groups
.iter()
.filter_map(|(id, _, metrics)| segment(id, metrics, report, 2, false, reading))
.collect()
}
fn name_segment(
content: &StripContent,
report: &Value,
selected: Option<&str>,
show_short_name: bool,
reading: UsageReading,
) -> Option<LogoSegment> {
let primary = report.get("primary").and_then(Value::as_str);
let starred = content.groups.iter().map(|(id, _, _)| id.as_str());
[selected, primary]
.into_iter()
.flatten()
.chain(starred)
.find_map(|id| {
let (id, _, metrics) = quota_group(report, id)?;
name_chip(&id, &metrics, report, show_short_name, reading)
})
}
fn name_chip(
id: &str,
metrics: &[StripMetric],
report: &Value,
show_short_name: bool,
reading: UsageReading,
) -> Option<LogoSegment> {
let highest = highest_metric(metrics)?;
segment(
id,
std::slice::from_ref(highest),
report,
1,
show_short_name,
reading,
)
}
fn highest_metric(metrics: &[StripMetric]) -> Option<&StripMetric> {
let shown = || metrics.iter().filter(|m| !m.value.trim().is_empty());
let highest_bounded =
shown()
.filter(|m| m.bounded)
.fold(None, |best: Option<&StripMetric>, m| match best {
Some(best) if best.fraction >= m.fraction => Some(best),
_ => Some(m),
});
highest_bounded.or_else(|| shown().next())
}
fn segment(
id: &str,
metrics: &[StripMetric],
report: &Value,
value_cap: usize,
with_name: bool,
reading: UsageReading,
) -> Option<LogoSegment> {
let values: Vec<String> = metrics
.iter()
.map(|metric| metric_text(metric, reading))
.filter(|value| !value.is_empty())
.map(str::to_owned)
.take(value_cap)
.collect();
if values.is_empty() {
return None;
}
let slug = id.split('@').next()?.to_ascii_lowercase();
if slug.is_empty() {
return None;
}
Some(LogoSegment {
slug,
short_name: short_name_for(report, id),
with_name,
values,
})
}
pub(super) fn tooltip(content: &StripContent, reading: UsageReading) -> String {
let lines: Vec<String> = content
.groups
.iter()
.filter_map(|(_, name, metrics)| {
let values: Vec<&str> = metrics
.iter()
.map(|metric| metric_text(metric, reading))
.filter(|value| !value.is_empty())
.collect();
if values.is_empty() {
return None;
}
Some(format!("{} · {}", safe_text(name), values.join(" ")))
})
.collect();
if lines.is_empty() {
"AI Usage".into()
} else {
lines.join("\n")
}
}
fn short_name_for(report: &Value, id: &str) -> Option<String> {
report
.get("entries")?
.as_array()?
.iter()
.find(|entry| entry.get("id").and_then(Value::as_str) == Some(id))?
.get("short_name")?
.as_str()
.map(str::trim)
.filter(|name| !name.is_empty())
.map(safe_text)
}
fn safe_text(value: &str) -> String {
value
.chars()
.filter(|c| !c.is_control())
.collect::<String>()
.trim()
.to_owned()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn empty_modes_fall_back_to_static_app_icon() {
for look in [MenuBarLook::Chart, MenuBarLook::Logos, MenuBarLook::Name] {
assert_eq!(
status_item_content(look, false),
StatusItemContent::AppIcon,
"{look:?}"
);
}
assert_eq!(
status_item_content(MenuBarLook::Chart, true),
StatusItemContent::Chart
);
assert_eq!(
status_item_content(MenuBarLook::Logos, true),
StatusItemContent::Logos
);
assert_eq!(
status_item_content(MenuBarLook::Name, true),
StatusItemContent::Logos
);
}
#[test]
fn menu_bar_look_round_trips_through_config_and_picker() {
for look in [MenuBarLook::Chart, MenuBarLook::Logos, MenuBarLook::Name] {
assert_eq!(MenuBarLook::from_style(Some(look.style())), look);
assert_eq!(
MenuBarLook::from_picker_value(look.picker_value()),
Some(look)
);
}
}
#[test]
fn menu_bar_look_defaults_to_the_chart() {
assert_eq!(MenuBarLook::from_style(None), MenuBarLook::Chart);
assert_eq!(MenuBarLook::from_style(Some("bars")), MenuBarLook::Chart);
assert_eq!(
MenuBarLook::from_style(Some("sparkles")),
MenuBarLook::Chart
);
assert_eq!(MenuBarLook::from_picker_value("sparkles"), None);
}
#[test]
fn fallback_menu_attaches_only_without_the_webview() {
assert!(!fallback_menu_attached(true));
assert!(fallback_menu_attached(false));
}
#[test]
fn fallback_menu_items_have_unique_nonempty_ids_ending_in_quit() {
let items = fallback_menu_items();
assert!(items.iter().all(|item| !item.id.is_empty()));
assert!(items.iter().all(|item| !item.label.is_empty()));
let ids: Vec<&str> = items.iter().map(|item| item.id).collect();
let mut unique = ids.clone();
unique.sort_unstable();
unique.dedup();
assert_eq!(ids.len(), unique.len(), "ids must be unique: {ids:?}");
assert_eq!(items[items.len() - 1].id, "fallback-quit");
assert_eq!(items[0].id, "fallback-refresh");
}
#[test]
fn logo_segments_keep_one_star_value_and_use_star_order() {
let content = StripContent {
groups: vec![
("anthropic".into(), "Claude".into(), vec![metric("41%")]),
(
"openai".into(),
"Codex".into(),
vec![metric("9%"), metric("5%")],
),
],
bars: Vec::new(),
};
let report = json!({"entries":[]});
let segments = logo_segments(
&content,
&report,
MenuBarLook::Logos,
None,
true,
UsageReading::Used,
);
assert_eq!(segments.len(), 2);
assert_eq!(segments[0].slug, "anthropic");
assert_eq!(segments[0].values, vec![String::from("41%")]);
assert_eq!(segments[1].slug, "openai");
assert_eq!(
segments[1].values,
vec![String::from("9%"), String::from("5%")]
);
}
#[test]
fn logo_segments_skip_groups_without_values() {
let content = StripContent {
groups: vec![
("cursor".into(), "Cursor".into(), vec![metric(" ")]),
("zai".into(), "Z.AI".into(), vec![metric("12%")]),
],
bars: Vec::new(),
};
let segments = logo_segments(
&content,
&json!({"entries":[]}),
MenuBarLook::Logos,
None,
true,
UsageReading::Used,
);
assert_eq!(segments.len(), 1);
assert_eq!(segments[0].slug, "zai");
assert_eq!(segments[0].values, vec![String::from("12%")]);
}
#[test]
fn content_without_metric_values_stays_empty_in_both_modes() {
let report = json!({"entries":[
{"id":"claude", "display_name":"Claude", "sections":[
{"type":"metric", "label":"Weekly"}
]}
]});
let content = super::super::strip::content_from_payload(
&report,
&super::super::strip::Stars::new(),
&[],
);
assert!(content.groups.is_empty());
assert!(content.bars.is_empty());
assert!(
logo_segments(
&content,
&report,
MenuBarLook::Logos,
None,
true,
UsageReading::Used
)
.is_empty()
);
}
#[test]
fn logo_segments_use_report_short_name_when_mark_is_unknown() {
let content = StripContent {
groups: vec![(
"unknown@work".into(),
"Unknown Provider".into(),
vec![metric("8%")],
)],
bars: Vec::new(),
};
let report = json!({"entries":[
{"id":"unknown@work", "short_name":"unk"}
]});
let segments = logo_segments(
&content,
&report,
MenuBarLook::Logos,
None,
true,
UsageReading::Used,
);
assert_eq!(segments[0].slug, "unknown");
assert!(super::super::marks::mark_svg(&segments[0].slug).is_none());
assert_eq!(segments[0].short_name.as_deref(), Some("unk"));
}
fn entry(id: &str, short_name: &str, windows: &[(&str, f64)]) -> Value {
let sections: Vec<Value> = windows
.iter()
.map(|(label, percent)| {
json!({"type":"metric", "label":label, "percent":percent,
"value":format!("{percent}%")})
})
.collect();
json!({"id":id, "short_name":short_name, "status":"ready", "sections":sections})
}
fn starred(ids: &[&str]) -> StripContent {
StripContent {
groups: ids
.iter()
.map(|id| (id.to_string(), id.to_string(), vec![metric("1%")]))
.collect(),
bars: Vec::new(),
}
}
#[test]
fn name_look_adds_the_short_name_beside_the_mark() {
let content = starred(&["anthropic"]);
let report = json!({"entries":[entry("anthropic", "cld", &[("Session", 41.0)])]});
let chip = logo_segments(
&content,
&report,
MenuBarLook::Name,
None,
true,
UsageReading::Used,
);
assert!(chip[0].with_name);
assert_eq!(chip[0].short_name.as_deref(), Some("cld"));
assert!(super::super::marks::mark_svg(&chip[0].slug).is_some());
let logos = logo_segments(
&content,
&report,
MenuBarLook::Logos,
None,
true,
UsageReading::Used,
);
assert!(!logos[0].with_name);
}
#[test]
fn name_look_can_leave_out_the_short_name() {
let content = starred(&["kimi", "unknown"]);
let report = json!({"primary":null,"entries":[
entry("kimi", "kmi", &[("Weekly", 42.0)]),
entry("unknown", "unk", &[("Usage", 8.0)]),
]});
let chip = logo_segments(
&content,
&report,
MenuBarLook::Name,
Some("kimi"),
false,
UsageReading::Used,
);
assert!(!chip[0].with_name);
assert!(super::super::marks::mark_svg(&chip[0].slug).is_some());
assert_eq!(chip[0].values, vec![String::from("42%")]);
let shown = logo_segments(
&content,
&report,
MenuBarLook::Name,
Some("kimi"),
true,
UsageReading::Used,
);
assert!(shown[0].with_name);
let unmarked = logo_segments(
&content,
&report,
MenuBarLook::Name,
Some("unknown"),
false,
UsageReading::Used,
);
assert!(super::super::marks::mark_svg(&unmarked[0].slug).is_none());
assert_eq!(unmarked[0].short_name.as_deref(), Some("unk"));
}
fn two_groups() -> StripContent {
StripContent {
groups: vec![
(
"anthropic".into(),
"Claude".into(),
vec![metric("54%"), metric("16%")],
),
("openai".into(), "Codex".into(), vec![metric("100%")]),
],
bars: Vec::new(),
}
}
fn two_entries(primary: Option<&str>) -> Value {
json!({"primary":primary, "entries":[
entry("anthropic", "cld", &[("Session", 54.0), ("Weekly", 16.0)]),
entry("openai", "gpt", &[("Session", 100.0)]),
]})
}
#[test]
fn name_look_shows_only_the_selected_provider_and_one_value() {
let report = two_entries(Some("anthropic"));
let chip = |selected| {
logo_segments(
&two_groups(),
&report,
MenuBarLook::Name,
selected,
true,
UsageReading::Used,
)
};
let codex = chip(Some("openai"));
assert_eq!(codex.len(), 1);
assert_eq!(codex[0].slug, "openai");
assert_eq!(codex[0].values, vec![String::from("100%")]);
assert_eq!(chip(Some("anthropic"))[0].values, vec![String::from("54%")]);
let logos = logo_segments(
&two_groups(),
&report,
MenuBarLook::Logos,
Some("openai"),
true,
UsageReading::Used,
);
assert_eq!(logos.len(), 2);
assert_eq!(logos[0].values.len(), 2);
}
#[test]
fn name_look_falls_back_from_selection_to_primary_to_first() {
let slug = |report: &Value, selected: Option<&str>| {
logo_segments(
&two_groups(),
report,
MenuBarLook::Name,
selected,
true,
UsageReading::Used,
)[0]
.slug
.clone()
};
assert_eq!(slug(&two_entries(Some("openai")), None), "openai");
assert_eq!(slug(&two_entries(Some("openai")), Some("gone")), "openai");
assert_eq!(slug(&two_entries(None), None), "anthropic");
assert_eq!(slug(&two_entries(None), Some("gone")), "anthropic");
}
#[test]
fn name_look_shows_a_selected_provider_that_has_no_star() {
let report = json!({"primary":"zai","entries":[
entry("zai", "zai", &[("Session", 0.0)]),
entry("supergrok", "sgk", &[("Weekly usage", 7.0)]),
]});
let content = starred(&["zai"]);
let chip = logo_segments(
&content,
&report,
MenuBarLook::Name,
Some("supergrok"),
true,
UsageReading::Used,
);
assert_eq!(chip.len(), 1);
assert_eq!(chip[0].slug, "supergrok");
assert_eq!(chip[0].short_name.as_deref(), Some("sgk"));
assert_eq!(chip[0].values, vec![String::from("7%")]);
let unselected = logo_segments(
&content,
&report,
MenuBarLook::Name,
None,
true,
UsageReading::Used,
);
assert_eq!(unselected[0].slug, "zai");
}
#[test]
fn name_look_skips_a_selected_provider_without_a_value() {
let report = json!({"primary":null,"entries":[
{"id":"cursor", "status":"ready", "sections":[{"type":"metric", "label":"Usage"}]},
{"id":"kimi", "status":"error", "sections":[
{"type":"metric", "label":"Weekly", "percent":40, "value":"40%"}]},
entry("zai", "zai", &[("Session", 12.0)]),
]});
let content = starred(&["cursor", "kimi", "zai"]);
for selected in ["cursor", "kimi"] {
let segments = logo_segments(
&content,
&report,
MenuBarLook::Name,
Some(selected),
true,
UsageReading::Used,
);
assert_eq!(segments.len(), 1);
assert_eq!(segments[0].slug, "zai");
}
}
#[test]
fn tooltip_lists_each_starred_group_and_uses_generic_empty_text() {
let content = StripContent {
groups: vec![
(
"anthropic".into(),
"Claude".into(),
vec![metric("41%"), metric("5%")],
),
("openai".into(), "Codex".into(), vec![metric("9%")]),
("cursor".into(), "Cursor".into(), vec![metric("")]),
],
bars: Vec::new(),
};
assert_eq!(
tooltip(&content, UsageReading::Used),
"Claude · 41% 5%\nCodex · 9%"
);
assert_eq!(
tooltip(
&StripContent {
groups: Vec::new(),
bars: Vec::new(),
},
UsageReading::Used
),
"AI Usage"
);
}
fn metric(value: &str) -> super::super::strip::StripMetric {
super::super::strip::StripMetric {
provider_id: String::new(),
provider_name: String::new(),
key: String::new(),
label: String::new(),
value: value.to_owned(),
fraction: 0.0,
bounded: true,
left_value: None,
grouped: false,
}
}
fn used(value: &str, fraction: f64) -> super::super::strip::StripMetric {
super::super::strip::StripMetric {
fraction,
..metric(value)
}
}
#[test]
fn name_look_shows_the_highest_quota_window() {
let chip = |zai: Value| {
let report = json!({"primary":null, "entries":[zai]});
logo_segments(
&starred(&["zai"]),
&report,
MenuBarLook::Name,
Some("zai"),
true,
UsageReading::Used,
)[0]
.values
.clone()
};
let spent_weekly = entry("zai", "zai", &[("Session (5h)", 0.0), ("Weekly", 100.0)]);
assert_eq!(chip(spent_weekly), vec![String::from("100%")]);
let monthly_third = entry(
"zai",
"zai",
&[
("Session (5h)", 0.0),
("Weekly", 0.0),
("MCP tools (monthly)", 18.0),
],
);
assert_eq!(chip(monthly_third), vec![String::from("18%")]);
let balance_first = json!({"id":"zai", "status":"ready", "sections":[
{"type":"metric", "label":"Balance", "value":"$40"},
{"type":"metric", "label":"Weekly", "percent":12, "value":"12%"}]});
assert_eq!(chip(balance_first), vec![String::from("12%")]);
let starred_session = StripContent {
groups: vec![("zai".into(), "Z.AI".into(), vec![used("0%", 0.0)])],
bars: Vec::new(),
};
let logos = logo_segments(
&starred_session,
&json!({"entries":[]}),
MenuBarLook::Logos,
None,
true,
UsageReading::Used,
);
assert_eq!(logos[0].values, vec![String::from("0%")]);
}
#[test]
fn name_look_ranks_grouped_rows_only_without_a_window() {
let report = json!({"primary":null, "entries":[
{"id":"anthropic", "status":"ready", "sections":[
{"type":"metric", "label":"Session", "percent":0, "value":"0%"},
{"type":"metric", "label":"cli-1", "group":"Sessions", "percent":90, "value":"90%"}]},
{"id":"supergrok", "status":"ready", "sections":[
{"type":"metric", "label":"Grok", "group":"Products", "percent":7, "value":"7%"}]},
]});
let content = starred(&["anthropic", "supergrok"]);
let value = |selected| {
logo_segments(
&content,
&report,
MenuBarLook::Name,
Some(selected),
true,
UsageReading::Used,
)[0]
.values
.clone()
};
assert_eq!(value("anthropic"), vec![String::from("0%")]);
assert_eq!(value("supergrok"), vec![String::from("7%")]);
}
#[test]
fn menu_bar_text_follows_the_left_reading() {
let report = json!({"primary":null, "entries":[
entry("zai", "zai", &[("Session (5h)", 0.0), ("Weekly", 0.0), ("MCP tools (monthly)", 18.0)]),
{"id":"openrouter", "status":"ready", "sections":[
{"type":"metric", "label":"Balance", "value":"$40"}]},
]});
let content = super::super::strip::content_from_payload(
&report,
&super::super::strip::Stars::new(),
&[],
);
let chip = |selected, reading| {
logo_segments(
&content,
&report,
MenuBarLook::Name,
Some(selected),
true,
reading,
)[0]
.values
.clone()
};
assert_eq!(chip("zai", UsageReading::Left), vec![String::from("82%")]);
assert_eq!(chip("zai", UsageReading::Used), vec![String::from("18%")]);
assert_eq!(
chip("openrouter", UsageReading::Left),
vec![String::from("$40")]
);
let logos = logo_segments(
&content,
&report,
MenuBarLook::Logos,
None,
true,
UsageReading::Left,
);
assert_eq!(
logos[0].values,
vec![String::from("100%"), String::from("100%")]
);
assert_eq!(
tooltip(&content, UsageReading::Left),
"zai · 100% 100%\nopenrouter · $40"
);
}
#[test]
fn usage_reading_comes_from_the_strip_ipc() {
let reading = |value: Value| UsageReading::from_strip_ipc(&value);
assert_eq!(reading(json!({"show_as":"left"})), UsageReading::Left);
assert_eq!(reading(json!({"show_as":"used"})), UsageReading::Used);
assert_eq!(reading(json!({"show_as":"sideways"})), UsageReading::Used);
assert_eq!(reading(json!({})), UsageReading::Used);
}
}