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
49pub type HiddenRows = BTreeMap<String, Vec<String>>;
51
52#[derive(Debug, Clone, PartialEq)]
54pub struct StripMetric {
55 pub provider_id: String,
56 pub provider_name: String,
57 pub key: String,
58 pub label: String,
59 pub value: String,
60 pub fraction: f64,
62 pub bounded: bool,
63 pub left_value: Option<String>,
67 pub value_headline: bool,
71 pub grouped: bool,
74}
75
76pub type StripGroup = (String, String, Vec<StripMetric>);
78
79#[derive(Debug, Clone, PartialEq)]
81pub struct StripContent {
82 pub groups: Vec<StripGroup>,
83 pub bars: Vec<StripMetric>,
84}
85
86impl StripContent {
87 pub fn is_empty(&self) -> bool {
88 self.groups.is_empty()
89 }
90
91 pub fn title_line(&self) -> String {
94 let mut parts = Vec::new();
95 for (_, name, metrics) in &self.groups {
96 let values: Vec<&str> = metrics.iter().map(|m| m.value.as_str()).collect();
97 if values.is_empty() {
98 continue;
99 }
100 parts.push(format!("{name} {}", values.join(" ")));
101 }
102 parts.join(" ")
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq)]
109pub struct BarFill {
110 pub fill_w: f64,
111 pub remainder_w: f64,
112 pub divider_x: Option<f64>,
113}
114
115pub fn visual_fraction(fraction: f64) -> f64 {
118 if !fraction.is_finite() {
119 return 0.0;
120 }
121 let clamped = fraction.clamp(0.0, 1.0);
122 if clamped > 0.7 && clamped < 1.0 {
123 let remainder = 1.0 - clamped;
124 let quantized = ((remainder / 0.15).ceil() * 0.15).min(1.0);
125 (1.0 - quantized).max(0.0)
126 } else {
127 clamped
128 }
129}
130
131pub fn bar_fill(track_w: f64, fraction: f64) -> BarFill {
132 if !fraction.is_finite() {
133 return BarFill {
134 fill_w: 0.0,
135 remainder_w: 0.0,
136 divider_x: None,
137 };
138 }
139 if fraction <= 0.0 {
140 return BarFill {
145 fill_w: 0.0,
146 remainder_w: track_w,
147 divider_x: Some(0.0),
148 };
149 }
150 let visual = visual_fraction(fraction);
151 if visual >= 1.0 {
152 return BarFill {
153 fill_w: track_w,
154 remainder_w: 0.0,
155 divider_x: None,
156 };
157 }
158 let min_visible = (track_w * 0.2).round().max(4.0);
159 let max_fill_w = (track_w - min_visible).max(1.0);
160 let fill_w = (track_w * visual).round().clamp(1.0, max_fill_w);
161 let true_remainder = track_w - fill_w;
162 let remainder_w = true_remainder.max(min_visible).min(track_w - 1.0);
163 BarFill {
164 fill_w,
165 remainder_w,
166 divider_x: Some(track_w - remainder_w),
167 }
168}
169
170pub fn parse_strip_ipc(value: &Value) -> (StripStyle, Stars, Vec<String>) {
173 let style = StripStyle::Bars;
174 let mut stars = Stars::new();
175 if let Some(map) = value.get("stars").and_then(Value::as_object) {
176 for (id, keys) in map {
177 let id = id.trim();
178 if id.is_empty() {
179 continue;
180 }
181 let mut list = Vec::new();
182 if let Some(arr) = keys.as_array() {
183 for key in arr {
184 let Some(key) = key.as_str() else { continue };
185 let key = key.trim();
186 if key.is_empty() || list.iter().any(|k| k == key) {
187 continue;
188 }
189 list.push(key.to_string());
190 if list.len() == MAX_STARS_PER_PROVIDER {
191 break;
192 }
193 }
194 }
195 if !list.is_empty() {
196 stars.insert(id.to_string(), list);
197 }
198 }
199 }
200 let mut order = Vec::new();
201 if let Some(arr) = value.get("order").and_then(Value::as_array) {
202 for id in arr {
203 let Some(id) = id.as_str() else { continue };
204 let id = id.trim();
205 if id.is_empty() || order.iter().any(|existing| existing == id) {
206 continue;
207 }
208 order.push(id.to_string());
209 }
210 }
211 (style, stars, order)
212}
213
214#[cfg_attr(not(any(target_os = "macos", test)), allow(dead_code))]
218pub fn parse_hidden_rows(value: &Value) -> HiddenRows {
219 let mut hidden = HiddenRows::new();
220 let Some(map) = value.get("hidden_rows").and_then(Value::as_object) else {
221 return hidden;
222 };
223 for (id, keys) in map {
224 let id = id.trim();
225 let Some(keys) = keys.as_array() else {
226 continue;
227 };
228 let mut list: Vec<String> = Vec::new();
229 for key in keys.iter().filter_map(Value::as_str).map(str::trim) {
230 if !key.is_empty() && !list.iter().any(|k| k == key) {
231 list.push(key.to_string());
232 }
233 }
234 if !id.is_empty() && !list.is_empty() {
235 hidden.insert(id.to_string(), list);
236 }
237 }
238 hidden
239}
240
241pub fn content_from_payload(payload: &Value, stars: &Stars, order: &[String]) -> StripContent {
246 let Some(entries) = payload.get("entries").and_then(Value::as_array) else {
247 return StripContent {
248 groups: Vec::new(),
249 bars: Vec::new(),
250 };
251 };
252 let mut by_id = std::collections::HashMap::new();
253 let mut payload_ids = Vec::new();
254 for entry in entries {
255 let Some(id) = entry.get("id").and_then(Value::as_str) else {
256 continue;
257 };
258 payload_ids.push(id.to_string());
259 by_id.insert(id.to_string(), entry);
260 }
261 let mut walk = Vec::new();
262 let mut seen = std::collections::HashSet::new();
263 for id in order {
264 if by_id.contains_key(id) && seen.insert(id.clone()) {
265 walk.push(id.clone());
266 }
267 }
268 for id in payload_ids {
269 if seen.insert(id.clone()) {
270 walk.push(id);
271 }
272 }
273 let groups: Vec<StripGroup> = walk
274 .into_iter()
275 .filter_map(|id| {
276 let entry = by_id.get(&id)?;
277 let starred =
278 (!stars.is_empty()).then(|| stars.get(&id).map(Vec::as_slice).unwrap_or_default());
279 entry_group(&id, entry, starred)
280 })
281 .collect();
282 let bars: Vec<StripMetric> = groups
283 .iter()
284 .flat_map(|(_, _, metrics)| metrics.iter().cloned())
285 .filter(|m| m.bounded)
286 .take(MAX_BARS)
287 .collect();
288 StripContent { groups, bars }
289}
290
291#[cfg_attr(not(any(target_os = "macos", test)), allow(dead_code))]
301pub fn quota_group(payload: &Value, id: &str, hidden: &[String]) -> Option<StripGroup> {
302 let entry = payload
303 .get("entries")?
304 .as_array()?
305 .iter()
306 .find(|entry| entry.get("id").and_then(Value::as_str) == Some(id))?;
307 if entry.get("status").and_then(Value::as_str) == Some("error") {
308 return None;
309 }
310 let name = entry_name(entry, id);
311 let shown: Vec<StripMetric> = metrics_for_entry(entry, id, &name)
312 .into_iter()
313 .filter(|m| !m.value.trim().is_empty() && !hidden.contains(&m.key))
314 .collect();
315 let windows: Vec<StripMetric> = shown.iter().filter(|m| !m.grouped).cloned().collect();
316 let metrics = if windows.is_empty() { shown } else { windows };
317 (!metrics.is_empty()).then(|| (id.to_string(), name, metrics))
318}
319
320fn entry_name(entry: &Value, id: &str) -> String {
321 entry
322 .get("display_name")
323 .or_else(|| entry.get("name"))
324 .and_then(Value::as_str)
325 .unwrap_or(id)
326 .to_string()
327}
328
329fn entry_group(id: &str, entry: &Value, starred: Option<&[String]>) -> Option<StripGroup> {
333 if entry.get("status").and_then(Value::as_str) == Some("error") {
334 return None;
335 }
336 let name = entry_name(entry, id);
337 let metrics = metrics_for_entry(entry, id, &name);
338 let wanted: Vec<String> = match starred {
339 Some(keys) => keys.to_vec(),
340 None => metrics
341 .iter()
342 .filter(|m| m.bounded)
343 .take(MAX_STARS_PER_PROVIDER)
344 .map(|m| m.key.clone())
345 .collect(),
346 };
347 let picked: Vec<StripMetric> = wanted
348 .iter()
349 .filter_map(|key| {
350 metrics
351 .iter()
352 .find(|m| &m.key == key && !m.value.trim().is_empty())
353 .cloned()
354 })
355 .collect();
356 (!picked.is_empty()).then(|| (id.to_string(), name, picked))
357}
358
359fn metrics_for_entry(entry: &Value, id: &str, name: &str) -> Vec<StripMetric> {
360 let Some(sections) = entry.get("sections").and_then(Value::as_array) else {
361 return Vec::new();
362 };
363 let mut group = String::new();
364 let mut rows = Vec::new();
365 let mut seen: BTreeMap<String, u32> = BTreeMap::new();
366 for section in sections {
367 let kind = section.get("type").and_then(Value::as_str).unwrap_or("");
368 if kind == "text" {
369 let label = section.get("label").and_then(Value::as_str).unwrap_or("");
370 let value = section.get("value").and_then(Value::as_str).unwrap_or("");
371 if !label.is_empty() && value.is_empty() {
372 group = label.to_string();
373 }
374 continue;
375 }
376 if kind != "metric" {
377 continue;
378 }
379 let raw_label = section.get("label").and_then(Value::as_str).unwrap_or("");
380 let field_group = section.get("group").and_then(Value::as_str).unwrap_or("");
385 let effective_group: &str = if field_group.is_empty() {
386 group.as_str()
387 } else {
388 field_group
389 };
390 let label = metric_label(id, raw_label);
391 let label = if effective_group.is_empty() {
392 label
393 } else {
394 format!("{label} ({effective_group})")
395 };
396 let percent = section.get("percent").and_then(Value::as_f64);
397 let value = section
398 .get("value")
399 .and_then(Value::as_str)
400 .filter(|value| !value.trim().is_empty())
401 .map(str::to_string)
402 .or_else(|| percent.map(|percent| format!("{}%", percent.round() as i64)))
403 .unwrap_or_default();
404 let value_headline = section.get("headline").and_then(Value::as_str) == Some("value");
405 let fraction = (percent.unwrap_or(0.0) / 100.0).clamp(0.0, 1.0);
406 let left_value = percent
407 .filter(|_| !value_headline)
408 .map(|percent| format!("{}%", (100.0 - percent).max(0.0).round() as i64));
409 let mut key = metric_key(id, raw_label, effective_group);
410 let count = seen.entry(key.clone()).or_insert(0);
411 *count += 1;
412 if *count > 1 {
413 key = format!("{key} #{count}");
414 }
415 rows.push(StripMetric {
416 provider_id: id.to_string(),
417 provider_name: name.to_string(),
418 key,
419 label,
420 value,
421 fraction,
422 bounded: true,
423 left_value,
424 value_headline,
425 grouped: !effective_group.is_empty(),
426 });
427 }
428 rows
429}
430
431fn metric_key(entry_id: &str, raw_label: &str, group: &str) -> String {
436 let label = metric_label(entry_id, raw_label);
437 if group.is_empty() {
438 format!("metric:{label}")
439 } else {
440 format!("metric:{label} ({group})")
441 }
442}
443
444fn metric_label(entry_id: &str, label: &str) -> String {
448 let slug = entry_id
449 .split('@')
450 .next()
451 .unwrap_or(entry_id)
452 .to_ascii_lowercase();
453 if slug != "supergrok" {
454 return label.to_string();
455 }
456 let label = strip_trailing_word(label, "build credits");
457 strip_trailing_word(&label, "usage")
458}
459
460fn strip_trailing_word(label: &str, word: &str) -> String {
464 if label.len() > word.len() && label.to_ascii_lowercase().ends_with(word) {
465 let head = &label[..label.len() - word.len()];
466 let trimmed = head.trim_end();
467 if trimmed.len() < head.len() {
468 return trimmed.to_string();
469 }
470 }
471 label.to_string()
472}
473
474#[derive(Debug, Clone, Copy)]
476pub struct BarsLayout {
477 pub n: usize,
478 pub track_x: f64,
479 pub track_w: f64,
480 pub track_h: f64,
481 pub rx: f64,
482 pub y_offset: f64,
483 pub gap: f64,
484}
485
486pub fn bars_layout(count: usize, side: f64) -> BarsLayout {
487 let n = count.clamp(1, MAX_BARS);
488 let pad = (side * 0.08).round().max(1.0);
489 let gap = (side * 0.03).round().max(1.0);
490 let track_x = pad;
491 let track_w = side - 2.0 * pad;
492 let layout_n = n.max(2) as f64;
493 let track_h = ((side - 2.0 * pad - (layout_n - 1.0) * gap) / layout_n)
494 .floor()
495 .max(1.0);
496 let rx = (track_h / 3.0).floor().max(1.0);
497 let total_h = n as f64 * track_h + (n as f64 - 1.0) * gap;
498 let y_offset = pad + ((side - 2.0 * pad - total_h) / 2.0).floor();
499 BarsLayout {
500 n,
501 track_x,
502 track_w,
503 track_h,
504 rx,
505 y_offset,
506 gap,
507 }
508}
509
510pub fn bars_rgba(fractions: &[f64], side: u32) -> Vec<u8> {
513 let side = side.max(8);
514 let mut buf = vec![0u8; side as usize * side as usize * 4];
515 if fractions.is_empty() {
516 return buf;
517 }
518 let size = f64::from(side);
519 let layout = bars_layout(fractions.len(), size);
520 let n = layout.n;
521
522 for (i, fraction) in fractions.iter().copied().take(n).enumerate() {
523 let y = layout.y_offset + i as f64 * (layout.track_h + layout.gap) + 1.0;
524 stamp_round_rect(
525 &mut buf,
526 side,
527 RoundBar {
528 x: layout.track_x,
529 y,
530 w: layout.track_w,
531 h: layout.track_h,
532 r_left: layout.rx,
533 r_right: layout.rx,
534 },
535 41, );
537 let fill = bar_fill(layout.track_w, fraction);
538 if fill.fill_w > 0.0 {
539 let trailing = if fill.fill_w >= layout.track_w {
540 layout.rx
541 } else {
542 (layout.rx * 0.35).floor().max(0.0)
543 };
544 stamp_round_rect(
545 &mut buf,
546 side,
547 RoundBar {
548 x: layout.track_x,
549 y,
550 w: fill.fill_w,
551 h: layout.track_h,
552 r_left: layout.rx,
553 r_right: trailing,
554 },
555 255,
556 );
557 }
558 if fill.remainder_w > 0.0
559 && let Some(divider_x) = fill.divider_x
560 {
561 let r_left = if divider_x <= 0.0 {
565 layout.rx
566 } else {
567 (layout.rx * 0.2).floor().max(0.0)
568 };
569 stamp_round_rect(
570 &mut buf,
571 side,
572 RoundBar {
573 x: layout.track_x + divider_x,
574 y,
575 w: fill.remainder_w,
576 h: layout.track_h,
577 r_left,
578 r_right: layout.rx,
579 },
580 61, );
582 }
583 }
584 buf
585}
586
587struct RoundBar {
588 x: f64,
589 y: f64,
590 w: f64,
591 h: f64,
592 r_left: f64,
593 r_right: f64,
594}
595
596fn stamp_round_rect(buf: &mut [u8], side: u32, bar: RoundBar, alpha: u8) {
597 if bar.w <= 0.0 || bar.h <= 0.0 {
598 return;
599 }
600 let side_i = side as i32;
601 let min_x = bar.x.floor().max(0.0) as i32;
602 let min_y = bar.y.floor().max(0.0) as i32;
603 let max_x = (bar.x + bar.w).ceil().min(f64::from(side)) as i32;
604 let max_y = (bar.y + bar.h).ceil().min(f64::from(side)) as i32;
605 let target = f64::from(alpha);
606 for py in min_y..max_y {
607 for px in min_x..max_x {
608 if px < 0 || py < 0 || px >= side_i || py >= side_i {
609 continue;
610 }
611 let cover = sample_round_rect(f64::from(px) + 0.5, f64::from(py) + 0.5, &bar);
612 if cover <= 0.0 {
613 continue;
614 }
615 let idx = ((py as u32 * side + px as u32) * 4) as usize;
616 let stamped = (target * cover).round().clamp(0.0, 255.0) as u8;
617 if stamped > buf[idx + 3] {
618 buf[idx] = 0;
619 buf[idx + 1] = 0;
620 buf[idx + 2] = 0;
621 buf[idx + 3] = stamped;
622 }
623 }
624 }
625}
626
627fn sample_round_rect(px: f64, py: f64, bar: &RoundBar) -> f64 {
628 (0.5 - sd_round_box(px, py, bar)).clamp(0.0, 1.0)
629}
630
631fn sd_round_box(px: f64, py: f64, bar: &RoundBar) -> f64 {
632 let r = if px < bar.x + bar.w * 0.5 {
633 bar.r_left
634 } else {
635 bar.r_right
636 };
637 let r = r.max(0.0).min(bar.h * 0.5).min(bar.w * 0.5);
638 let cx = bar.x + bar.w * 0.5;
639 let cy = bar.y + bar.h * 0.5;
640 let dx = (px - cx).abs() - (bar.w * 0.5 - r);
641 let dy = (py - cy).abs() - (bar.h * 0.5 - r);
642 let outside = (dx.max(0.0).powi(2) + dy.max(0.0).powi(2)).sqrt();
643 outside + dx.min(0.0).max(dy.min(0.0)) - r
644}
645
646#[cfg(test)]
647mod tests {
648 use super::*;
649 use serde_json::json;
650
651 #[test]
652 fn zero_or_negative_fraction_draws_no_fill_but_a_visible_track() {
653 let empty = bar_fill(100.0, 0.0);
654 assert_eq!(empty.fill_w, 0.0);
655 assert_eq!(empty.remainder_w, 100.0);
656 assert_eq!(empty.divider_x, Some(0.0));
657 assert_eq!(bar_fill(100.0, -0.5).fill_w, 0.0);
658 let nan = bar_fill(100.0, f64::NAN);
660 assert_eq!(nan.remainder_w, 0.0);
661 assert_eq!(nan.divider_x, None);
662 }
663
664 #[test]
665 fn full_fraction_fills_track_with_no_remainder() {
666 let fill = bar_fill(100.0, 1.0);
667 assert_eq!(fill.fill_w, 100.0);
668 assert_eq!(fill.remainder_w, 0.0);
669 assert_eq!(fill.divider_x, None);
670 }
671
672 #[test]
673 fn near_full_keeps_a_visible_tail() {
674 let fill = bar_fill(100.0, 0.97);
675 assert!(fill.fill_w < 100.0);
676 assert!(fill.remainder_w >= 20.0);
677 assert_eq!(fill.divider_x, Some(fill.fill_w));
678 }
679
680 #[test]
681 fn visual_fraction_quantizes_near_full_in_fifteen_percent_steps() {
682 assert!((visual_fraction(0.97) - 0.85).abs() < 0.0001);
683 assert_eq!(visual_fraction(1.0), 1.0);
684 assert_eq!(visual_fraction(0.0), 0.0);
685 assert!((visual_fraction(0.5) - 0.5).abs() < 0.0001);
686 }
687
688 fn sample_payload() -> Value {
689 json!({
690 "entries": [
691 {
692 "id": "anthropic",
693 "display_name": "Claude",
694 "status": "ready",
695 "sections": [
696 {"type": "metric", "label": "Weekly", "percent": 19, "value": "19%"},
697 {"type": "metric", "label": "Session", "percent": 41, "value": "41%"}
698 ]
699 },
700 {
701 "id": "openai",
702 "display_name": "Codex",
703 "status": "ready",
704 "sections": [
705 {"type": "metric", "label": "Codex weekly", "percent": 2, "value": "2%"}
706 ]
707 },
708 {
709 "id": "cursor",
710 "display_name": "Cursor",
711 "status": "error",
712 "sections": [
713 {"type": "metric", "label": "Cursor Models", "percent": 90, "value": "90%"}
714 ]
715 }
716 ]
717 })
718 }
719
720 #[test]
723 fn metric_keys_match_the_popover_fixture() {
724 let fixture: Value =
725 serde_json::from_str(include_str!("../../tests/fixtures/strip_metric_keys.json"))
726 .expect("fixture is JSON");
727 let cases = fixture["cases"].as_array().expect("cases");
728 assert!(!cases.is_empty());
729 for case in cases {
730 let id = case["id"].as_str().unwrap();
731 let label = case["label"].as_str().unwrap();
732 let group = case["group"].as_str().unwrap_or("");
733 assert_eq!(
734 metric_key(id, label, group),
735 case["key"].as_str().unwrap(),
736 "{case}"
737 );
738 }
739 }
740
741 #[test]
745 fn a_starred_supergrok_meter_is_painted() {
746 let payload = serde_json::json!({ "entries": [
747 { "id": "supergrok", "status": "ready", "sections": [
748 { "type": "metric", "label": "Weekly usage", "percent": 3 }
749 ]}
750 ]});
751 let mut stars = Stars::new();
752 stars.insert("supergrok".into(), vec!["metric:Weekly".into()]);
753 let content = content_from_payload(&payload, &stars, &["supergrok".into()]);
754 assert_eq!(content.bars.len(), 1);
755 assert_eq!(content.bars[0].key, "metric:Weekly");
756 }
757
758 #[test]
759 fn empty_stars_fall_back_to_first_two_bounded_metrics() {
760 let content = content_from_payload(&sample_payload(), &Stars::new(), &[]);
761 assert_eq!(content.groups.len(), 2);
762 assert_eq!(content.groups[0].2.len(), 2);
763 assert_eq!(content.groups[0].2[0].key, "metric:Weekly");
764 assert_eq!(content.groups[0].2[1].key, "metric:Session");
765 assert_eq!(content.groups[1].2[0].key, "metric:Codex weekly");
766 assert_eq!(content.bars.len(), 3);
767 assert!(!content.title_line().is_empty());
768 }
769
770 #[test]
771 fn stars_select_named_metrics_and_skip_errors() {
772 let mut stars = Stars::new();
773 stars.insert("anthropic".into(), vec!["metric:Session".into()]);
774 stars.insert("cursor".into(), vec!["metric:Cursor Models".into()]);
775 let content = content_from_payload(&sample_payload(), &stars, &[]);
776 assert_eq!(content.groups.len(), 1);
777 assert_eq!(content.groups[0].2[0].key, "metric:Session");
778 assert_eq!(content.groups[0].2[0].fraction, 0.41);
779 assert_eq!(content.bars.len(), 1);
780 }
781
782 #[test]
783 fn grouped_metric_keys_match_the_popover() {
784 let payload = json!({
785 "entries": [{
786 "id": "antigravity",
787 "display_name": "Antigravity",
788 "status": "ready",
789 "sections": [
790 {"type": "text", "label": "Session", "value": ""},
791 {"type": "metric", "label": "Gemini", "percent": 4, "value": "4%"},
792 {"type": "text", "label": "Weekly", "value": ""},
793 {"type": "metric", "label": "Gemini", "percent": 11, "value": "11%"}
794 ]
795 }]
796 });
797 let content = content_from_payload(&payload, &Stars::new(), &[]);
798 assert_eq!(content.groups[0].2[0].key, "metric:Gemini (Session)");
799 assert_eq!(content.groups[0].2[1].key, "metric:Gemini (Weekly)");
800 }
801
802 #[test]
807 fn metric_level_groups_key_like_positional_ones() {
808 let payload = json!({
809 "entries": [{
810 "id": "anthropic",
811 "display_name": "Claude",
812 "status": "ready",
813 "sections": [
814 {"type": "metric", "label": "Weekly (7d)", "percent": 32, "value": "32%"},
815 {"type": "metric", "label": "ship the release", "percent": 91,
816 "value": "91%", "group": "Sessions"},
817 {"type": "metric", "label": "Grok Build", "percent": 94,
818 "value": "94%", "group": "Breakdown"}
819 ]
820 }]
821 });
822 let mut stars = Stars::new();
823 stars.insert(
824 "anthropic".into(),
825 vec![
826 "metric:ship the release (Sessions)".into(),
827 "metric:Grok Build (Breakdown)".into(),
828 ],
829 );
830 let content = content_from_payload(&payload, &stars, &[]);
831 let bars = &content.groups[0].2;
832 assert_eq!(bars.len(), 2);
833 assert_eq!(bars[0].key, "metric:ship the release (Sessions)");
834 assert_eq!(bars[0].label, "ship the release (Sessions)");
835 assert_eq!(bars[1].key, "metric:Grok Build (Breakdown)");
836 assert_eq!(bars[1].label, "Grok Build (Breakdown)");
837 }
838
839 #[test]
842 fn left_value_is_the_remaining_percent() {
843 let payload = json!({"entries":[{"id":"zai", "status":"ready", "sections":[
844 {"type":"metric", "label":"Weekly", "percent":18, "value":"18%"},
845 {"type":"metric", "label":"Over", "percent":130, "value":"130%"},
846 {"type":"metric", "label":"Credits", "percent":40, "value":"$4 of $10", "headline":"value"},
847 {"type":"metric", "label":"Balance", "value":"$40"}]}]});
848 let (_, _, metrics) = quota_group(&payload, "zai", &[]).unwrap();
849 let left: Vec<Option<&str>> = metrics.iter().map(|m| m.left_value.as_deref()).collect();
850 assert_eq!(left, vec![Some("82%"), Some("0%"), None, None]);
851 }
852
853 #[test]
857 fn quota_group_leaves_out_hidden_metrics() {
858 let payload = json!({"entries":[{"id":"zai", "status":"ready", "sections":[
859 {"type":"metric", "label":"Session (5h)", "percent":0, "value":"0%"},
860 {"type":"metric", "label":"MCP tools (monthly)", "percent":18, "value":"18%"},
861 {"type":"metric", "label":"Glm", "group":"Models", "percent":40, "value":"40%"}]}]});
862 let keys = |hidden: &[&str]| {
863 let hidden: Vec<String> = hidden.iter().map(|k| k.to_string()).collect();
864 quota_group(&payload, "zai", &hidden)
865 .map(|(_, _, metrics)| metrics.into_iter().map(|m| m.key).collect::<Vec<_>>())
866 };
867
868 assert_eq!(
869 keys(&[]),
870 Some(vec![
871 "metric:Session (5h)".to_string(),
872 "metric:MCP tools (monthly)".to_string()
873 ])
874 );
875 assert_eq!(
876 keys(&["metric:MCP tools (monthly)"]),
877 Some(vec!["metric:Session (5h)".to_string()])
878 );
879 assert_eq!(
880 keys(&["metric:Session (5h)", "metric:MCP tools (monthly)"]),
881 Some(vec!["metric:Glm (Models)".to_string()])
882 );
883 assert_eq!(
884 keys(&[
885 "metric:Session (5h)",
886 "metric:MCP tools (monthly)",
887 "metric:Glm (Models)"
888 ]),
889 None
890 );
891 }
892
893 #[test]
896 fn parse_hidden_rows_reads_the_strip_ipc() {
897 let hidden = parse_hidden_rows(&json!({"hidden_rows": {
898 "zai": [" metric:MCP tools (monthly) ", "metric:MCP tools (monthly)", "", 7],
899 "": ["metric:Weekly"],
900 "openai": [],
901 "kimi": "metric:Weekly"
902 }}));
903 assert_eq!(hidden.len(), 1);
904 assert_eq!(
905 hidden["zai"],
906 vec!["metric:MCP tools (monthly)".to_string()]
907 );
908 assert!(parse_hidden_rows(&json!({"stars": {}})).is_empty());
909 assert!(parse_hidden_rows(&json!({"hidden_rows": ["zai"]})).is_empty());
910 }
911
912 #[test]
913 fn parse_strip_ipc_caps_stars_at_two_per_provider() {
914 let value = json!({
915 "style": "text",
916 "stars": {
917 "anthropic": ["metric:Weekly", "metric:Session", "metric:Extra"],
918 "": ["x"],
919 "openai": ["metric:Codex weekly"]
920 }
921 });
922 let (style, stars, order) = parse_strip_ipc(&value);
923 assert_eq!(style, StripStyle::Bars);
924 assert_eq!(stars["anthropic"].len(), 2);
925 assert_eq!(stars["openai"], vec!["metric:Codex weekly".to_string()]);
926 assert!(!stars.contains_key(""));
927 assert!(order.is_empty());
928 }
929
930 #[test]
931 fn strip_follows_popover_card_order() {
932 let mut stars = Stars::new();
933 stars.insert("anthropic".into(), vec!["metric:Weekly".into()]);
934 stars.insert("openai".into(), vec!["metric:Codex weekly".into()]);
935 let content = content_from_payload(
936 &sample_payload(),
937 &stars,
938 &["openai".into(), "anthropic".into()],
939 );
940 assert_eq!(content.groups[0].0, "openai");
941 assert_eq!(content.groups[1].0, "anthropic");
942 assert_eq!(content.bars[0].provider_id, "openai");
943 assert_eq!(content.bars[1].provider_id, "anthropic");
944 }
945
946 #[test]
947 fn bars_rgba_is_square_black_and_anti_aliased() {
948 let bytes = bars_rgba(&[0.4, 0.97], BARS_PIXEL_SIDE);
949 assert_eq!(
950 bytes.len(),
951 (BARS_PIXEL_SIDE * BARS_PIXEL_SIDE * 4) as usize
952 );
953 assert_eq!(bytes[3], 0, "top-left stays transparent");
954 let last = bytes.len() - 1;
955 assert_eq!(bytes[last], 0, "bottom-right stays transparent");
956 let mut inked = 0usize;
957 let mut soft = 0usize;
958 for pixel in bytes.as_chunks::<4>().0 {
959 if pixel[3] > 0 {
960 inked += 1;
961 assert_eq!(&pixel[..3], &[0, 0, 0]);
962 }
963 if pixel[3] > 0 && pixel[3] < 255 {
964 soft += 1;
965 }
966 }
967 assert!(inked > 40, "glyph too sparse: {inked}");
968 assert!(soft > 0, "expected anti-aliased edges");
969 }
970
971 #[test]
972 fn empty_fractions_are_fully_transparent() {
973 let bytes = bars_rgba(&[], BARS_PIXEL_SIDE);
974 assert!(bytes.as_chunks::<4>().0.iter().all(|p| p[3] == 0));
975 }
976
977 #[test]
982 fn empty_bars_keep_a_visible_capsule() {
983 let bytes = bars_rgba(&[0.0, 0.5], BARS_PIXEL_SIDE);
984 let side = BARS_PIXEL_SIDE as usize;
985 let layout = bars_layout(2, f64::from(BARS_PIXEL_SIDE));
986 let y = (layout.y_offset + 1.0 + layout.track_h / 2.0) as usize;
987 let mut peak = 0u8;
988 let mut visible_cols = 0usize;
989 for x in 0..side {
990 let alpha = bytes[(y * side + x) * 4 + 3];
991 peak = peak.max(alpha);
992 if alpha >= 61 {
993 visible_cols += 1;
994 }
995 }
996 assert_eq!(peak, 61, "empty row peaks at the remainder alpha");
997 assert!(
998 visible_cols > side / 2,
999 "capsule spans the track: {visible_cols}"
1000 );
1001 }
1002
1003 #[test]
1004 fn bars_rgba_at_each_status_item_scale() {
1005 for scale in BARS_SCALES {
1006 let side = BARS_POINT_SIDE * scale;
1007 let bytes = bars_rgba(&[0.4, 0.97], side);
1008 assert_eq!(bytes.len(), (side * side * 4) as usize, "scale {scale}");
1009 assert!(
1010 bytes.as_chunks::<4>().0.iter().any(|p| p[3] > 0),
1011 "scale {scale} produced an empty glyph"
1012 );
1013 }
1014 }
1015}