1use std::collections::BTreeMap;
5
6use serde_json::Value;
7
8pub const MAX_STARS_PER_PROVIDER: usize = 2;
10pub const MAX_BARS: usize = 4;
12pub const BARS_POINT_SIDE: u32 = 18;
14pub const BARS_PIXEL_SIDE: u32 = BARS_POINT_SIDE * 2;
17#[cfg_attr(not(test), allow(dead_code))]
20pub const BARS_SCALES: [u32; 3] = [1, 2, 3];
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum StripStyle {
25 Text,
26 Bars,
27}
28
29impl StripStyle {
30 pub fn parse(name: &str) -> Self {
31 if name.eq_ignore_ascii_case("text") {
32 Self::Text
33 } else {
34 Self::Bars
35 }
36 }
37
38 pub fn as_str(self) -> &'static str {
39 match self {
40 Self::Text => "text",
41 Self::Bars => "bars",
42 }
43 }
44}
45
46pub type Stars = BTreeMap<String, Vec<String>>;
48
49#[derive(Debug, Clone, PartialEq)]
51pub struct StripMetric {
52 pub provider_id: String,
53 pub provider_name: String,
54 pub key: String,
55 pub label: String,
56 pub value: String,
57 pub fraction: f64,
59 pub bounded: bool,
60 pub left_value: Option<String>,
64 pub grouped: bool,
67}
68
69pub type StripGroup = (String, String, Vec<StripMetric>);
71
72#[derive(Debug, Clone, PartialEq)]
74pub struct StripContent {
75 pub groups: Vec<StripGroup>,
76 pub bars: Vec<StripMetric>,
77}
78
79impl StripContent {
80 pub fn is_empty(&self) -> bool {
81 self.groups.is_empty()
82 }
83
84 pub fn title_line(&self) -> String {
87 let mut parts = Vec::new();
88 for (_, name, metrics) in &self.groups {
89 let values: Vec<&str> = metrics.iter().map(|m| m.value.as_str()).collect();
90 if values.is_empty() {
91 continue;
92 }
93 parts.push(format!("{name} {}", values.join(" ")));
94 }
95 parts.join(" ")
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq)]
102pub struct BarFill {
103 pub fill_w: f64,
104 pub remainder_w: f64,
105 pub divider_x: Option<f64>,
106}
107
108pub fn visual_fraction(fraction: f64) -> f64 {
111 if !fraction.is_finite() {
112 return 0.0;
113 }
114 let clamped = fraction.clamp(0.0, 1.0);
115 if clamped > 0.7 && clamped < 1.0 {
116 let remainder = 1.0 - clamped;
117 let quantized = ((remainder / 0.15).ceil() * 0.15).min(1.0);
118 (1.0 - quantized).max(0.0)
119 } else {
120 clamped
121 }
122}
123
124pub fn bar_fill(track_w: f64, fraction: f64) -> BarFill {
125 if !fraction.is_finite() || fraction <= 0.0 {
126 return BarFill {
127 fill_w: 0.0,
128 remainder_w: 0.0,
129 divider_x: None,
130 };
131 }
132 let visual = visual_fraction(fraction);
133 if visual >= 1.0 {
134 return BarFill {
135 fill_w: track_w,
136 remainder_w: 0.0,
137 divider_x: None,
138 };
139 }
140 let min_visible = (track_w * 0.2).round().max(4.0);
141 let max_fill_w = (track_w - min_visible).max(1.0);
142 let fill_w = (track_w * visual).round().clamp(1.0, max_fill_w);
143 let true_remainder = track_w - fill_w;
144 let remainder_w = true_remainder.max(min_visible).min(track_w - 1.0);
145 BarFill {
146 fill_w,
147 remainder_w,
148 divider_x: Some(track_w - remainder_w),
149 }
150}
151
152pub fn parse_strip_ipc(value: &Value) -> (StripStyle, Stars, Vec<String>) {
155 let style = StripStyle::Bars;
156 let mut stars = Stars::new();
157 if let Some(map) = value.get("stars").and_then(Value::as_object) {
158 for (id, keys) in map {
159 let id = id.trim();
160 if id.is_empty() {
161 continue;
162 }
163 let mut list = Vec::new();
164 if let Some(arr) = keys.as_array() {
165 for key in arr {
166 let Some(key) = key.as_str() else { continue };
167 let key = key.trim();
168 if key.is_empty() || list.iter().any(|k| k == key) {
169 continue;
170 }
171 list.push(key.to_string());
172 if list.len() == MAX_STARS_PER_PROVIDER {
173 break;
174 }
175 }
176 }
177 if !list.is_empty() {
178 stars.insert(id.to_string(), list);
179 }
180 }
181 }
182 let mut order = Vec::new();
183 if let Some(arr) = value.get("order").and_then(Value::as_array) {
184 for id in arr {
185 let Some(id) = id.as_str() else { continue };
186 let id = id.trim();
187 if id.is_empty() || order.iter().any(|existing| existing == id) {
188 continue;
189 }
190 order.push(id.to_string());
191 }
192 }
193 (style, stars, order)
194}
195
196pub fn content_from_payload(payload: &Value, stars: &Stars, order: &[String]) -> StripContent {
201 let Some(entries) = payload.get("entries").and_then(Value::as_array) else {
202 return StripContent {
203 groups: Vec::new(),
204 bars: Vec::new(),
205 };
206 };
207 let mut by_id = std::collections::HashMap::new();
208 let mut payload_ids = Vec::new();
209 for entry in entries {
210 let Some(id) = entry.get("id").and_then(Value::as_str) else {
211 continue;
212 };
213 payload_ids.push(id.to_string());
214 by_id.insert(id.to_string(), entry);
215 }
216 let mut walk = Vec::new();
217 let mut seen = std::collections::HashSet::new();
218 for id in order {
219 if by_id.contains_key(id) && seen.insert(id.clone()) {
220 walk.push(id.clone());
221 }
222 }
223 for id in payload_ids {
224 if seen.insert(id.clone()) {
225 walk.push(id);
226 }
227 }
228 let groups: Vec<StripGroup> = walk
229 .into_iter()
230 .filter_map(|id| {
231 let entry = by_id.get(&id)?;
232 let starred =
233 (!stars.is_empty()).then(|| stars.get(&id).map(Vec::as_slice).unwrap_or_default());
234 entry_group(&id, entry, starred)
235 })
236 .collect();
237 let bars: Vec<StripMetric> = groups
238 .iter()
239 .flat_map(|(_, _, metrics)| metrics.iter().cloned())
240 .filter(|m| m.bounded)
241 .take(MAX_BARS)
242 .collect();
243 StripContent { groups, bars }
244}
245
246#[cfg_attr(not(any(target_os = "macos", test)), allow(dead_code))]
254pub fn quota_group(payload: &Value, id: &str) -> Option<StripGroup> {
255 let entry = payload
256 .get("entries")?
257 .as_array()?
258 .iter()
259 .find(|entry| entry.get("id").and_then(Value::as_str) == Some(id))?;
260 if entry.get("status").and_then(Value::as_str) == Some("error") {
261 return None;
262 }
263 let name = entry_name(entry, id);
264 let shown: Vec<StripMetric> = metrics_for_entry(entry, id, &name)
265 .into_iter()
266 .filter(|m| !m.value.trim().is_empty())
267 .collect();
268 let windows: Vec<StripMetric> = shown.iter().filter(|m| !m.grouped).cloned().collect();
269 let metrics = if windows.is_empty() { shown } else { windows };
270 (!metrics.is_empty()).then(|| (id.to_string(), name, metrics))
271}
272
273fn entry_name(entry: &Value, id: &str) -> String {
274 entry
275 .get("display_name")
276 .or_else(|| entry.get("name"))
277 .and_then(Value::as_str)
278 .unwrap_or(id)
279 .to_string()
280}
281
282fn entry_group(id: &str, entry: &Value, starred: Option<&[String]>) -> Option<StripGroup> {
286 if entry.get("status").and_then(Value::as_str) == Some("error") {
287 return None;
288 }
289 let name = entry_name(entry, id);
290 let metrics = metrics_for_entry(entry, id, &name);
291 let wanted: Vec<String> = match starred {
292 Some(keys) => keys.to_vec(),
293 None => metrics
294 .iter()
295 .filter(|m| m.bounded)
296 .take(MAX_STARS_PER_PROVIDER)
297 .map(|m| m.key.clone())
298 .collect(),
299 };
300 let picked: Vec<StripMetric> = wanted
301 .iter()
302 .filter_map(|key| {
303 metrics
304 .iter()
305 .find(|m| &m.key == key && !m.value.trim().is_empty())
306 .cloned()
307 })
308 .collect();
309 (!picked.is_empty()).then(|| (id.to_string(), name, picked))
310}
311
312fn metrics_for_entry(entry: &Value, id: &str, name: &str) -> Vec<StripMetric> {
313 let Some(sections) = entry.get("sections").and_then(Value::as_array) else {
314 return Vec::new();
315 };
316 let mut group = String::new();
317 let mut rows = Vec::new();
318 let mut seen: BTreeMap<String, u32> = BTreeMap::new();
319 for section in sections {
320 let kind = section.get("type").and_then(Value::as_str).unwrap_or("");
321 if kind == "text" {
322 let label = section.get("label").and_then(Value::as_str).unwrap_or("");
323 let value = section.get("value").and_then(Value::as_str).unwrap_or("");
324 if !label.is_empty() && value.is_empty() {
325 group = label.to_string();
326 }
327 continue;
328 }
329 if kind != "metric" {
330 continue;
331 }
332 let raw_label = section.get("label").and_then(Value::as_str).unwrap_or("");
333 let field_group = section.get("group").and_then(Value::as_str).unwrap_or("");
338 let effective_group: &str = if field_group.is_empty() {
339 group.as_str()
340 } else {
341 field_group
342 };
343 let label = metric_label(id, raw_label);
344 let label = if effective_group.is_empty() {
345 label
346 } else {
347 format!("{label} ({effective_group})")
348 };
349 let percent = section.get("percent").and_then(Value::as_f64);
350 let value = section
351 .get("value")
352 .and_then(Value::as_str)
353 .filter(|value| !value.trim().is_empty())
354 .map(str::to_string)
355 .or_else(|| percent.map(|percent| format!("{}%", percent.round() as i64)))
356 .unwrap_or_default();
357 let left_value = percent
358 .filter(|_| section.get("headline").and_then(Value::as_str) != Some("value"))
359 .map(|percent| format!("{}%", (100.0 - percent).max(0.0).round() as i64));
360 let mut key = metric_key(id, raw_label, effective_group);
361 let count = seen.entry(key.clone()).or_insert(0);
362 *count += 1;
363 if *count > 1 {
364 key = format!("{key} #{count}");
365 }
366 rows.push(StripMetric {
367 provider_id: id.to_string(),
368 provider_name: name.to_string(),
369 key,
370 label,
371 value,
372 fraction: (percent.unwrap_or(0.0) / 100.0).clamp(0.0, 1.0),
373 bounded: true,
374 left_value,
375 grouped: !effective_group.is_empty(),
376 });
377 }
378 rows
379}
380
381fn metric_key(entry_id: &str, raw_label: &str, group: &str) -> String {
386 let label = metric_label(entry_id, raw_label);
387 if group.is_empty() {
388 format!("metric:{label}")
389 } else {
390 format!("metric:{label} ({group})")
391 }
392}
393
394fn metric_label(entry_id: &str, label: &str) -> String {
398 let slug = entry_id
399 .split('@')
400 .next()
401 .unwrap_or(entry_id)
402 .to_ascii_lowercase();
403 if slug != "supergrok" {
404 return label.to_string();
405 }
406 let label = strip_trailing_word(label, "build credits");
407 strip_trailing_word(&label, "usage")
408}
409
410fn strip_trailing_word(label: &str, word: &str) -> String {
414 if label.len() > word.len() && label.to_ascii_lowercase().ends_with(word) {
415 let head = &label[..label.len() - word.len()];
416 let trimmed = head.trim_end();
417 if trimmed.len() < head.len() {
418 return trimmed.to_string();
419 }
420 }
421 label.to_string()
422}
423
424#[derive(Debug, Clone, Copy)]
426pub struct BarsLayout {
427 pub n: usize,
428 pub track_x: f64,
429 pub track_w: f64,
430 pub track_h: f64,
431 pub rx: f64,
432 pub y_offset: f64,
433 pub gap: f64,
434}
435
436pub fn bars_layout(count: usize, side: f64) -> BarsLayout {
437 let n = count.clamp(1, MAX_BARS);
438 let pad = (side * 0.08).round().max(1.0);
439 let gap = (side * 0.03).round().max(1.0);
440 let track_x = pad;
441 let track_w = side - 2.0 * pad;
442 let layout_n = n.max(2) as f64;
443 let track_h = ((side - 2.0 * pad - (layout_n - 1.0) * gap) / layout_n)
444 .floor()
445 .max(1.0);
446 let rx = (track_h / 3.0).floor().max(1.0);
447 let total_h = n as f64 * track_h + (n as f64 - 1.0) * gap;
448 let y_offset = pad + ((side - 2.0 * pad - total_h) / 2.0).floor();
449 BarsLayout {
450 n,
451 track_x,
452 track_w,
453 track_h,
454 rx,
455 y_offset,
456 gap,
457 }
458}
459
460pub fn bars_rgba(fractions: &[f64], side: u32) -> Vec<u8> {
463 let side = side.max(8);
464 let mut buf = vec![0u8; side as usize * side as usize * 4];
465 if fractions.is_empty() {
466 return buf;
467 }
468 let size = f64::from(side);
469 let layout = bars_layout(fractions.len(), size);
470 let n = layout.n;
471
472 for (i, fraction) in fractions.iter().copied().take(n).enumerate() {
473 let y = layout.y_offset + i as f64 * (layout.track_h + layout.gap) + 1.0;
474 stamp_round_rect(
475 &mut buf,
476 side,
477 RoundBar {
478 x: layout.track_x,
479 y,
480 w: layout.track_w,
481 h: layout.track_h,
482 r_left: layout.rx,
483 r_right: layout.rx,
484 },
485 41, );
487 let fill = bar_fill(layout.track_w, fraction);
488 if fill.fill_w > 0.0 {
489 let trailing = if fill.fill_w >= layout.track_w {
490 layout.rx
491 } else {
492 (layout.rx * 0.35).floor().max(0.0)
493 };
494 stamp_round_rect(
495 &mut buf,
496 side,
497 RoundBar {
498 x: layout.track_x,
499 y,
500 w: fill.fill_w,
501 h: layout.track_h,
502 r_left: layout.rx,
503 r_right: trailing,
504 },
505 255,
506 );
507 }
508 if fill.fill_w > 0.0
509 && fill.remainder_w > 0.0
510 && let Some(divider_x) = fill.divider_x
511 {
512 stamp_round_rect(
513 &mut buf,
514 side,
515 RoundBar {
516 x: layout.track_x + divider_x,
517 y,
518 w: fill.remainder_w,
519 h: layout.track_h,
520 r_left: (layout.rx * 0.2).floor().max(0.0),
521 r_right: layout.rx,
522 },
523 61, );
525 }
526 }
527 buf
528}
529
530struct RoundBar {
531 x: f64,
532 y: f64,
533 w: f64,
534 h: f64,
535 r_left: f64,
536 r_right: f64,
537}
538
539fn stamp_round_rect(buf: &mut [u8], side: u32, bar: RoundBar, alpha: u8) {
540 if bar.w <= 0.0 || bar.h <= 0.0 {
541 return;
542 }
543 let side_i = side as i32;
544 let min_x = bar.x.floor().max(0.0) as i32;
545 let min_y = bar.y.floor().max(0.0) as i32;
546 let max_x = (bar.x + bar.w).ceil().min(f64::from(side)) as i32;
547 let max_y = (bar.y + bar.h).ceil().min(f64::from(side)) as i32;
548 let target = f64::from(alpha);
549 for py in min_y..max_y {
550 for px in min_x..max_x {
551 if px < 0 || py < 0 || px >= side_i || py >= side_i {
552 continue;
553 }
554 let cover = sample_round_rect(f64::from(px) + 0.5, f64::from(py) + 0.5, &bar);
555 if cover <= 0.0 {
556 continue;
557 }
558 let idx = ((py as u32 * side + px as u32) * 4) as usize;
559 let stamped = (target * cover).round().clamp(0.0, 255.0) as u8;
560 if stamped > buf[idx + 3] {
561 buf[idx] = 0;
562 buf[idx + 1] = 0;
563 buf[idx + 2] = 0;
564 buf[idx + 3] = stamped;
565 }
566 }
567 }
568}
569
570fn sample_round_rect(px: f64, py: f64, bar: &RoundBar) -> f64 {
571 (0.5 - sd_round_box(px, py, bar)).clamp(0.0, 1.0)
572}
573
574fn sd_round_box(px: f64, py: f64, bar: &RoundBar) -> f64 {
575 let r = if px < bar.x + bar.w * 0.5 {
576 bar.r_left
577 } else {
578 bar.r_right
579 };
580 let r = r.max(0.0).min(bar.h * 0.5).min(bar.w * 0.5);
581 let cx = bar.x + bar.w * 0.5;
582 let cy = bar.y + bar.h * 0.5;
583 let dx = (px - cx).abs() - (bar.w * 0.5 - r);
584 let dy = (py - cy).abs() - (bar.h * 0.5 - r);
585 let outside = (dx.max(0.0).powi(2) + dy.max(0.0).powi(2)).sqrt();
586 outside + dx.min(0.0).max(dy.min(0.0)) - r
587}
588
589#[cfg(test)]
590mod tests {
591 use super::*;
592 use serde_json::json;
593
594 #[test]
595 fn zero_or_negative_fraction_draws_nothing() {
596 assert_eq!(bar_fill(100.0, 0.0).fill_w, 0.0);
597 assert_eq!(bar_fill(100.0, -0.5).fill_w, 0.0);
598 }
599
600 #[test]
601 fn full_fraction_fills_track_with_no_remainder() {
602 let fill = bar_fill(100.0, 1.0);
603 assert_eq!(fill.fill_w, 100.0);
604 assert_eq!(fill.remainder_w, 0.0);
605 assert_eq!(fill.divider_x, None);
606 }
607
608 #[test]
609 fn near_full_keeps_a_visible_tail() {
610 let fill = bar_fill(100.0, 0.97);
611 assert!(fill.fill_w < 100.0);
612 assert!(fill.remainder_w >= 20.0);
613 assert_eq!(fill.divider_x, Some(fill.fill_w));
614 }
615
616 #[test]
617 fn visual_fraction_quantizes_near_full_in_fifteen_percent_steps() {
618 assert!((visual_fraction(0.97) - 0.85).abs() < 0.0001);
619 assert_eq!(visual_fraction(1.0), 1.0);
620 assert_eq!(visual_fraction(0.0), 0.0);
621 assert!((visual_fraction(0.5) - 0.5).abs() < 0.0001);
622 }
623
624 fn sample_payload() -> Value {
625 json!({
626 "entries": [
627 {
628 "id": "anthropic",
629 "display_name": "Claude",
630 "status": "ready",
631 "sections": [
632 {"type": "metric", "label": "Weekly", "percent": 19, "value": "19%"},
633 {"type": "metric", "label": "Session", "percent": 41, "value": "41%"}
634 ]
635 },
636 {
637 "id": "openai",
638 "display_name": "Codex",
639 "status": "ready",
640 "sections": [
641 {"type": "metric", "label": "Codex weekly", "percent": 2, "value": "2%"}
642 ]
643 },
644 {
645 "id": "cursor",
646 "display_name": "Cursor",
647 "status": "error",
648 "sections": [
649 {"type": "metric", "label": "Cursor Models", "percent": 90, "value": "90%"}
650 ]
651 }
652 ]
653 })
654 }
655
656 #[test]
659 fn metric_keys_match_the_popover_fixture() {
660 let fixture: Value =
661 serde_json::from_str(include_str!("../../tests/fixtures/strip_metric_keys.json"))
662 .expect("fixture is JSON");
663 let cases = fixture["cases"].as_array().expect("cases");
664 assert!(!cases.is_empty());
665 for case in cases {
666 let id = case["id"].as_str().unwrap();
667 let label = case["label"].as_str().unwrap();
668 let group = case["group"].as_str().unwrap_or("");
669 assert_eq!(
670 metric_key(id, label, group),
671 case["key"].as_str().unwrap(),
672 "{case}"
673 );
674 }
675 }
676
677 #[test]
681 fn a_starred_supergrok_meter_is_painted() {
682 let payload = serde_json::json!({ "entries": [
683 { "id": "supergrok", "status": "ready", "sections": [
684 { "type": "metric", "label": "Weekly usage", "percent": 3 }
685 ]}
686 ]});
687 let mut stars = Stars::new();
688 stars.insert("supergrok".into(), vec!["metric:Weekly".into()]);
689 let content = content_from_payload(&payload, &stars, &["supergrok".into()]);
690 assert_eq!(content.bars.len(), 1);
691 assert_eq!(content.bars[0].key, "metric:Weekly");
692 }
693
694 #[test]
695 fn empty_stars_fall_back_to_first_two_bounded_metrics() {
696 let content = content_from_payload(&sample_payload(), &Stars::new(), &[]);
697 assert_eq!(content.groups.len(), 2);
698 assert_eq!(content.groups[0].2.len(), 2);
699 assert_eq!(content.groups[0].2[0].key, "metric:Weekly");
700 assert_eq!(content.groups[0].2[1].key, "metric:Session");
701 assert_eq!(content.groups[1].2[0].key, "metric:Codex weekly");
702 assert_eq!(content.bars.len(), 3);
703 assert!(!content.title_line().is_empty());
704 }
705
706 #[test]
707 fn stars_select_named_metrics_and_skip_errors() {
708 let mut stars = Stars::new();
709 stars.insert("anthropic".into(), vec!["metric:Session".into()]);
710 stars.insert("cursor".into(), vec!["metric:Cursor Models".into()]);
711 let content = content_from_payload(&sample_payload(), &stars, &[]);
712 assert_eq!(content.groups.len(), 1);
713 assert_eq!(content.groups[0].2[0].key, "metric:Session");
714 assert_eq!(content.groups[0].2[0].fraction, 0.41);
715 assert_eq!(content.bars.len(), 1);
716 }
717
718 #[test]
719 fn grouped_metric_keys_match_the_popover() {
720 let payload = json!({
721 "entries": [{
722 "id": "antigravity",
723 "display_name": "Antigravity",
724 "status": "ready",
725 "sections": [
726 {"type": "text", "label": "Session", "value": ""},
727 {"type": "metric", "label": "Gemini", "percent": 4, "value": "4%"},
728 {"type": "text", "label": "Weekly", "value": ""},
729 {"type": "metric", "label": "Gemini", "percent": 11, "value": "11%"}
730 ]
731 }]
732 });
733 let content = content_from_payload(&payload, &Stars::new(), &[]);
734 assert_eq!(content.groups[0].2[0].key, "metric:Gemini (Session)");
735 assert_eq!(content.groups[0].2[1].key, "metric:Gemini (Weekly)");
736 }
737
738 #[test]
743 fn metric_level_groups_key_like_positional_ones() {
744 let payload = json!({
745 "entries": [{
746 "id": "anthropic",
747 "display_name": "Claude",
748 "status": "ready",
749 "sections": [
750 {"type": "metric", "label": "Weekly (7d)", "percent": 32, "value": "32%"},
751 {"type": "metric", "label": "ship the release", "percent": 91,
752 "value": "91%", "group": "Sessions"},
753 {"type": "metric", "label": "Grok Build", "percent": 94,
754 "value": "94%", "group": "Breakdown"}
755 ]
756 }]
757 });
758 let mut stars = Stars::new();
759 stars.insert(
760 "anthropic".into(),
761 vec![
762 "metric:ship the release (Sessions)".into(),
763 "metric:Grok Build (Breakdown)".into(),
764 ],
765 );
766 let content = content_from_payload(&payload, &stars, &[]);
767 let bars = &content.groups[0].2;
768 assert_eq!(bars.len(), 2);
769 assert_eq!(bars[0].key, "metric:ship the release (Sessions)");
770 assert_eq!(bars[0].label, "ship the release (Sessions)");
771 assert_eq!(bars[1].key, "metric:Grok Build (Breakdown)");
772 assert_eq!(bars[1].label, "Grok Build (Breakdown)");
773 }
774
775 #[test]
778 fn left_value_is_the_remaining_percent() {
779 let payload = json!({"entries":[{"id":"zai", "status":"ready", "sections":[
780 {"type":"metric", "label":"Weekly", "percent":18, "value":"18%"},
781 {"type":"metric", "label":"Over", "percent":130, "value":"130%"},
782 {"type":"metric", "label":"Credits", "percent":40, "value":"$4 of $10", "headline":"value"},
783 {"type":"metric", "label":"Balance", "value":"$40"}]}]});
784 let (_, _, metrics) = quota_group(&payload, "zai").unwrap();
785 let left: Vec<Option<&str>> = metrics.iter().map(|m| m.left_value.as_deref()).collect();
786 assert_eq!(left, vec![Some("82%"), Some("0%"), None, None]);
787 }
788
789 #[test]
790 fn parse_strip_ipc_caps_stars_at_two_per_provider() {
791 let value = json!({
792 "style": "text",
793 "stars": {
794 "anthropic": ["metric:Weekly", "metric:Session", "metric:Extra"],
795 "": ["x"],
796 "openai": ["metric:Codex weekly"]
797 }
798 });
799 let (style, stars, order) = parse_strip_ipc(&value);
800 assert_eq!(style, StripStyle::Bars);
801 assert_eq!(stars["anthropic"].len(), 2);
802 assert_eq!(stars["openai"], vec!["metric:Codex weekly".to_string()]);
803 assert!(!stars.contains_key(""));
804 assert!(order.is_empty());
805 }
806
807 #[test]
808 fn strip_follows_popover_card_order() {
809 let mut stars = Stars::new();
810 stars.insert("anthropic".into(), vec!["metric:Weekly".into()]);
811 stars.insert("openai".into(), vec!["metric:Codex weekly".into()]);
812 let content = content_from_payload(
813 &sample_payload(),
814 &stars,
815 &["openai".into(), "anthropic".into()],
816 );
817 assert_eq!(content.groups[0].0, "openai");
818 assert_eq!(content.groups[1].0, "anthropic");
819 assert_eq!(content.bars[0].provider_id, "openai");
820 assert_eq!(content.bars[1].provider_id, "anthropic");
821 }
822
823 #[test]
824 fn bars_rgba_is_square_black_and_anti_aliased() {
825 let bytes = bars_rgba(&[0.4, 0.97], BARS_PIXEL_SIDE);
826 assert_eq!(
827 bytes.len(),
828 (BARS_PIXEL_SIDE * BARS_PIXEL_SIDE * 4) as usize
829 );
830 assert_eq!(bytes[3], 0, "top-left stays transparent");
831 let last = bytes.len() - 1;
832 assert_eq!(bytes[last], 0, "bottom-right stays transparent");
833 let mut inked = 0usize;
834 let mut soft = 0usize;
835 for pixel in bytes.as_chunks::<4>().0 {
836 if pixel[3] > 0 {
837 inked += 1;
838 assert_eq!(&pixel[..3], &[0, 0, 0]);
839 }
840 if pixel[3] > 0 && pixel[3] < 255 {
841 soft += 1;
842 }
843 }
844 assert!(inked > 40, "glyph too sparse: {inked}");
845 assert!(soft > 0, "expected anti-aliased edges");
846 }
847
848 #[test]
849 fn empty_fractions_are_fully_transparent() {
850 let bytes = bars_rgba(&[], BARS_PIXEL_SIDE);
851 assert!(bytes.as_chunks::<4>().0.iter().all(|p| p[3] == 0));
852 }
853
854 #[test]
855 fn bars_rgba_at_each_status_item_scale() {
856 for scale in BARS_SCALES {
857 let side = BARS_POINT_SIDE * scale;
858 let bytes = bars_rgba(&[0.4, 0.97], side);
859 assert_eq!(bytes.len(), (side * side * 4) as usize, "scale {scale}");
860 assert!(
861 bytes.as_chunks::<4>().0.iter().any(|p| p[3] > 0),
862 "scale {scale} produced an empty glyph"
863 );
864 }
865 }
866}