1#![cfg_attr(
5 not(any(target_os = "macos", target_os = "windows", test)),
6 allow(dead_code)
7)]
8
9use std::collections::BTreeMap;
10
11use serde_json::Value;
12
13pub const MAX_STARS_PER_PROVIDER: usize = 2;
15pub const MAX_BARS: usize = 4;
17pub const BARS_POINT_SIDE: u32 = 18;
19pub const BARS_PIXEL_SIDE: u32 = BARS_POINT_SIDE * 2;
22#[cfg_attr(not(test), allow(dead_code))]
25pub const BARS_SCALES: [u32; 3] = [1, 2, 3];
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum StripStyle {
30 Text,
31 Bars,
32}
33
34impl StripStyle {
35 pub fn parse(name: &str) -> Self {
36 if name.eq_ignore_ascii_case("text") {
37 Self::Text
38 } else {
39 Self::Bars
40 }
41 }
42
43 pub fn as_str(self) -> &'static str {
44 match self {
45 Self::Text => "text",
46 Self::Bars => "bars",
47 }
48 }
49}
50
51pub type Stars = BTreeMap<String, Vec<String>>;
53
54pub type HiddenRows = BTreeMap<String, Vec<String>>;
56
57#[derive(Debug, Clone, PartialEq)]
59pub struct StripMetric {
60 pub provider_id: String,
61 pub provider_name: String,
62 pub key: String,
63 pub label: String,
64 pub value: String,
65 pub fraction: f64,
67 pub bounded: bool,
68 pub left_value: Option<String>,
72 pub value_headline: bool,
76 pub grouped: bool,
79 pub model_pool: bool,
82}
83
84pub type StripGroup = (String, String, Vec<StripMetric>);
86
87#[derive(Debug, Clone, PartialEq)]
89pub struct StripContent {
90 pub groups: Vec<StripGroup>,
91 pub bars: Vec<StripMetric>,
92}
93
94impl StripContent {
95 pub fn is_empty(&self) -> bool {
96 self.groups.is_empty()
97 }
98
99 pub fn title_line(&self) -> String {
102 let mut parts = Vec::new();
103 for (_, name, metrics) in &self.groups {
104 let values: Vec<&str> = metrics.iter().map(|m| m.value.as_str()).collect();
105 if values.is_empty() {
106 continue;
107 }
108 parts.push(format!("{name} {}", values.join(" ")));
109 }
110 parts.join(" ")
111 }
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum UsageReading {
117 Used,
118 Left,
119}
120
121impl UsageReading {
122 pub fn from_strip_ipc(value: &Value) -> Self {
125 match value.get("show_as").and_then(Value::as_str) {
126 Some("left") => Self::Left,
127 _ => Self::Used,
128 }
129 }
130}
131
132pub fn metric_text(metric: &StripMetric, reading: UsageReading) -> &str {
135 match (reading, metric.left_value.as_deref()) {
136 (UsageReading::Left, Some(left)) => left,
137 _ => metric.value.trim(),
138 }
139}
140
141pub fn safe_text(value: &str) -> String {
143 value
144 .chars()
145 .filter(|c| !c.is_control())
146 .collect::<String>()
147 .trim()
148 .to_owned()
149}
150
151pub fn tooltip(content: &StripContent, reading: UsageReading) -> String {
153 let lines: Vec<String> = content
154 .groups
155 .iter()
156 .filter_map(|(_, name, metrics)| {
157 let values: Vec<String> = metrics
158 .iter()
159 .map(|metric| safe_text(metric_text(metric, reading)))
160 .filter(|value| !value.is_empty())
161 .collect();
162 if values.is_empty() {
163 return None;
164 }
165 Some(format!("{} · {}", safe_text(name), values.join(" ")))
166 })
167 .collect();
168 if lines.is_empty() {
169 "AI Usage".into()
170 } else {
171 lines.join("\n")
172 }
173}
174
175#[cfg(any(windows, test))]
179pub fn truncate_utf16(value: &str, max_units: usize) -> String {
180 let mut units = 0;
181 value
182 .chars()
183 .take_while(|ch| {
184 let next = units + ch.len_utf16();
185 if next > max_units {
186 return false;
187 }
188 units = next;
189 true
190 })
191 .collect()
192}
193
194#[derive(Debug, Clone, Copy, PartialEq)]
197pub struct BarFill {
198 pub fill_w: f64,
199 pub remainder_w: f64,
200 pub divider_x: Option<f64>,
201}
202
203pub fn visual_fraction(fraction: f64) -> f64 {
206 if !fraction.is_finite() {
207 return 0.0;
208 }
209 let clamped = fraction.clamp(0.0, 1.0);
210 if clamped > 0.7 && clamped < 1.0 {
211 let remainder = 1.0 - clamped;
212 let quantized = ((remainder / 0.15).ceil() * 0.15).min(1.0);
213 (1.0 - quantized).max(0.0)
214 } else {
215 clamped
216 }
217}
218
219pub fn bar_fill(track_w: f64, fraction: f64) -> BarFill {
220 if !fraction.is_finite() {
221 return BarFill {
222 fill_w: 0.0,
223 remainder_w: 0.0,
224 divider_x: None,
225 };
226 }
227 if fraction <= 0.0 {
228 return BarFill {
233 fill_w: 0.0,
234 remainder_w: track_w,
235 divider_x: Some(0.0),
236 };
237 }
238 let visual = visual_fraction(fraction);
239 if visual >= 1.0 {
240 return BarFill {
241 fill_w: track_w,
242 remainder_w: 0.0,
243 divider_x: None,
244 };
245 }
246 let min_visible = (track_w * 0.2).round().max(4.0);
247 let max_fill_w = (track_w - min_visible).max(1.0);
248 let fill_w = (track_w * visual).round().clamp(1.0, max_fill_w);
249 let true_remainder = track_w - fill_w;
250 let remainder_w = true_remainder.max(min_visible).min(track_w - 1.0);
251 BarFill {
252 fill_w,
253 remainder_w,
254 divider_x: Some(track_w - remainder_w),
255 }
256}
257
258pub fn parse_strip_ipc(value: &Value) -> (StripStyle, Stars, Vec<String>) {
261 let style = StripStyle::Bars;
262 let mut stars = Stars::new();
263 if let Some(map) = value.get("stars").and_then(Value::as_object) {
264 for (id, keys) in map {
265 let id = id.trim();
266 if id.is_empty() {
267 continue;
268 }
269 let mut list = Vec::new();
270 if let Some(arr) = keys.as_array() {
271 for key in arr {
272 let Some(key) = key.as_str() else { continue };
273 let key = key.trim();
274 if key.is_empty() || list.iter().any(|k| k == key) {
275 continue;
276 }
277 list.push(key.to_string());
278 if list.len() == MAX_STARS_PER_PROVIDER {
279 break;
280 }
281 }
282 }
283 stars.insert(id.to_string(), list);
287 }
288 }
289 let mut order = Vec::new();
290 if let Some(arr) = value.get("order").and_then(Value::as_array) {
291 for id in arr {
292 let Some(id) = id.as_str() else { continue };
293 let id = id.trim();
294 if id.is_empty() || order.iter().any(|existing| existing == id) {
295 continue;
296 }
297 order.push(id.to_string());
298 }
299 }
300 (style, stars, order)
301}
302
303#[cfg_attr(not(any(target_os = "macos", test)), allow(dead_code))]
307pub fn parse_hidden_rows(value: &Value) -> HiddenRows {
308 let mut hidden = HiddenRows::new();
309 let Some(map) = value.get("hidden_rows").and_then(Value::as_object) else {
310 return hidden;
311 };
312 for (id, keys) in map {
313 let id = id.trim();
314 let Some(keys) = keys.as_array() else {
315 continue;
316 };
317 let mut list: Vec<String> = Vec::new();
318 for key in keys.iter().filter_map(Value::as_str).map(str::trim) {
319 if !key.is_empty() && !list.iter().any(|k| k == key) {
320 list.push(key.to_string());
321 }
322 }
323 if !id.is_empty() && !list.is_empty() {
324 hidden.insert(id.to_string(), list);
325 }
326 }
327 hidden
328}
329
330pub fn content_from_payload(payload: &Value, stars: &Stars, order: &[String]) -> StripContent {
335 let Some(entries) = payload.get("entries").and_then(Value::as_array) else {
336 return StripContent {
337 groups: Vec::new(),
338 bars: Vec::new(),
339 };
340 };
341 let mut by_id = std::collections::HashMap::new();
342 let mut payload_ids = Vec::new();
343 for entry in entries {
344 let Some(id) = entry.get("id").and_then(Value::as_str) else {
345 continue;
346 };
347 payload_ids.push(id.to_string());
348 by_id.insert(id.to_string(), entry);
349 }
350 let mut walk = Vec::new();
351 let mut seen = std::collections::HashSet::new();
352 for id in order {
353 if by_id.contains_key(id) && seen.insert(id.clone()) {
354 walk.push(id.clone());
355 }
356 }
357 for id in payload_ids {
358 if seen.insert(id.clone()) {
359 walk.push(id);
360 }
361 }
362 let groups: Vec<StripGroup> = walk
363 .into_iter()
364 .filter_map(|id| {
365 let entry = by_id.get(&id)?;
366 let starred =
367 (!stars.is_empty()).then(|| stars.get(&id).map(Vec::as_slice).unwrap_or_default());
368 entry_group(&id, entry, starred)
369 })
370 .collect();
371 let bars: Vec<StripMetric> = groups
372 .iter()
373 .flat_map(|(_, _, metrics)| metrics.iter().cloned())
374 .filter(|m| m.bounded)
375 .take(MAX_BARS)
376 .collect();
377 StripContent { groups, bars }
378}
379
380#[cfg_attr(not(any(target_os = "macos", test)), allow(dead_code))]
392pub fn quota_group(payload: &Value, id: &str, hidden: &[String]) -> Option<StripGroup> {
393 let entry = payload
394 .get("entries")?
395 .as_array()?
396 .iter()
397 .find(|entry| entry.get("id").and_then(Value::as_str) == Some(id))?;
398 if entry.get("status").and_then(Value::as_str) == Some("error") {
399 return None;
400 }
401 let name = entry_name(entry, id);
402 let shown: Vec<StripMetric> = metrics_for_entry(entry, id, &name)
403 .into_iter()
404 .filter(|m| !m.value.trim().is_empty() && !hidden.contains(&m.key))
405 .collect();
406 let shared: Vec<StripMetric> = shown.iter().filter(|m| !m.model_pool).cloned().collect();
407 let shown = if shared.is_empty() { shown } else { shared };
408 let windows: Vec<StripMetric> = shown.iter().filter(|m| !m.grouped).cloned().collect();
409 let metrics = if windows.is_empty() { shown } else { windows };
410 (!metrics.is_empty()).then(|| (id.to_string(), name, metrics))
411}
412
413fn entry_name(entry: &Value, id: &str) -> String {
414 entry
415 .get("display_name")
416 .or_else(|| entry.get("name"))
417 .and_then(Value::as_str)
418 .unwrap_or(id)
419 .to_string()
420}
421
422fn entry_group(id: &str, entry: &Value, starred: Option<&[String]>) -> Option<StripGroup> {
426 if entry.get("status").and_then(Value::as_str) == Some("error") {
427 return None;
428 }
429 let name = entry_name(entry, id);
430 let metrics = metrics_for_entry(entry, id, &name);
431 let wanted: Vec<String> = match starred {
432 Some(keys) => keys.to_vec(),
433 None => metrics
434 .iter()
435 .filter(|m| m.bounded)
436 .take(MAX_STARS_PER_PROVIDER)
437 .map(|m| m.key.clone())
438 .collect(),
439 };
440 let picked: Vec<StripMetric> = wanted
441 .iter()
442 .filter_map(|key| {
443 metrics
444 .iter()
445 .find(|m| &m.key == key && !m.value.trim().is_empty())
446 .cloned()
447 })
448 .collect();
449 (!picked.is_empty()).then(|| (id.to_string(), name, picked))
450}
451
452fn metrics_for_entry(entry: &Value, id: &str, name: &str) -> Vec<StripMetric> {
453 let Some(sections) = entry.get("sections").and_then(Value::as_array) else {
454 return Vec::new();
455 };
456 let mut group = String::new();
457 let mut rows = Vec::new();
458 let mut seen: BTreeMap<String, u32> = BTreeMap::new();
459 for section in sections {
460 let kind = section.get("type").and_then(Value::as_str).unwrap_or("");
461 if kind == "text" {
462 let label = section.get("label").and_then(Value::as_str).unwrap_or("");
463 let value = section.get("value").and_then(Value::as_str).unwrap_or("");
464 if !label.is_empty() && value.is_empty() {
465 group = label.to_string();
466 }
467 continue;
468 }
469 if kind != "metric" {
470 continue;
471 }
472 let raw_label = section.get("label").and_then(Value::as_str).unwrap_or("");
473 let field_group = section.get("group").and_then(Value::as_str).unwrap_or("");
478 let effective_group: &str = if field_group.is_empty() {
479 group.as_str()
480 } else {
481 field_group
482 };
483 let label = metric_label(id, raw_label);
484 let label = if effective_group.is_empty() {
485 label
486 } else {
487 format!("{label} ({effective_group})")
488 };
489 let percent = section.get("percent").and_then(Value::as_f64);
490 let value = section
491 .get("value")
492 .and_then(Value::as_str)
493 .filter(|value| !value.trim().is_empty())
494 .map(str::to_string)
495 .or_else(|| percent.map(|percent| format!("{}%", percent.round() as i64)))
496 .unwrap_or_default();
497 let value_headline = section.get("headline").and_then(Value::as_str) == Some("value");
498 let fraction = (percent.unwrap_or(0.0) / 100.0).clamp(0.0, 1.0);
499 let left_value = percent
500 .filter(|_| !value_headline)
501 .map(|percent| format!("{}%", (100.0 - percent).max(0.0).round() as i64));
502 let mut key = metric_key(id, raw_label, effective_group);
503 let count = seen.entry(key.clone()).or_insert(0);
504 *count += 1;
505 if *count > 1 {
506 key = format!("{key} #{count}");
507 }
508 rows.push(StripMetric {
509 provider_id: id.to_string(),
510 provider_name: name.to_string(),
511 key,
512 label,
513 value,
514 fraction,
515 bounded: true,
516 left_value,
517 value_headline,
518 grouped: !effective_group.is_empty(),
519 model_pool: section.get("model_pool").and_then(Value::as_bool) == Some(true),
520 });
521 }
522 rows
523}
524
525fn metric_key(entry_id: &str, raw_label: &str, group: &str) -> String {
530 let label = metric_label(entry_id, raw_label);
531 if group.is_empty() {
532 format!("metric:{label}")
533 } else {
534 format!("metric:{label} ({group})")
535 }
536}
537
538fn metric_label(entry_id: &str, label: &str) -> String {
542 let slug = entry_id
543 .split('@')
544 .next()
545 .unwrap_or(entry_id)
546 .to_ascii_lowercase();
547 if slug != "supergrok" {
548 return label.to_string();
549 }
550 let label = strip_trailing_word(label, "build credits");
551 strip_trailing_word(&label, "usage")
552}
553
554fn strip_trailing_word(label: &str, word: &str) -> String {
558 if label.len() > word.len() && label.to_ascii_lowercase().ends_with(word) {
559 let head = &label[..label.len() - word.len()];
560 let trimmed = head.trim_end();
561 if trimmed.len() < head.len() {
562 return trimmed.to_string();
563 }
564 }
565 label.to_string()
566}
567
568#[derive(Debug, Clone, Copy)]
570pub struct BarsLayout {
571 pub n: usize,
572 pub track_x: f64,
573 pub track_w: f64,
574 pub track_h: f64,
575 pub rx: f64,
576 pub y_offset: f64,
577 pub gap: f64,
578}
579
580pub fn bars_layout(count: usize, side: f64) -> BarsLayout {
581 let n = count.clamp(1, MAX_BARS);
582 let pad = (side * 0.08).round().max(1.0);
583 let gap = (side * 0.03).round().max(1.0);
584 let track_x = pad;
585 let track_w = side - 2.0 * pad;
586 let layout_n = n.max(2) as f64;
587 let track_h = ((side - 2.0 * pad - (layout_n - 1.0) * gap) / layout_n)
588 .floor()
589 .max(1.0);
590 let rx = (track_h / 3.0).floor().max(1.0);
591 let total_h = n as f64 * track_h + (n as f64 - 1.0) * gap;
592 let y_offset = pad + ((side - 2.0 * pad - total_h) / 2.0).floor();
593 BarsLayout {
594 n,
595 track_x,
596 track_w,
597 track_h,
598 rx,
599 y_offset,
600 gap,
601 }
602}
603
604pub fn bars_rgba(fractions: &[f64], side: u32) -> Vec<u8> {
607 let side = side.max(8);
608 let mut buf = vec![0u8; side as usize * side as usize * 4];
609 if fractions.is_empty() {
610 return buf;
611 }
612 let size = f64::from(side);
613 let layout = bars_layout(fractions.len(), size);
614 let n = layout.n;
615
616 for (i, fraction) in fractions.iter().copied().take(n).enumerate() {
617 let y = layout.y_offset + i as f64 * (layout.track_h + layout.gap) + 1.0;
618 stamp_round_rect(
619 &mut buf,
620 side,
621 RoundBar {
622 x: layout.track_x,
623 y,
624 w: layout.track_w,
625 h: layout.track_h,
626 r_left: layout.rx,
627 r_right: layout.rx,
628 },
629 41, );
631 let fill = bar_fill(layout.track_w, fraction);
632 if fill.fill_w > 0.0 {
633 let trailing = if fill.fill_w >= layout.track_w {
634 layout.rx
635 } else {
636 (layout.rx * 0.35).floor().max(0.0)
637 };
638 stamp_round_rect(
639 &mut buf,
640 side,
641 RoundBar {
642 x: layout.track_x,
643 y,
644 w: fill.fill_w,
645 h: layout.track_h,
646 r_left: layout.rx,
647 r_right: trailing,
648 },
649 255,
650 );
651 }
652 if fill.remainder_w > 0.0
653 && let Some(divider_x) = fill.divider_x
654 {
655 let r_left = if divider_x <= 0.0 {
659 layout.rx
660 } else {
661 (layout.rx * 0.2).floor().max(0.0)
662 };
663 stamp_round_rect(
664 &mut buf,
665 side,
666 RoundBar {
667 x: layout.track_x + divider_x,
668 y,
669 w: fill.remainder_w,
670 h: layout.track_h,
671 r_left,
672 r_right: layout.rx,
673 },
674 61, );
676 }
677 }
678 buf
679}
680
681struct RoundBar {
682 x: f64,
683 y: f64,
684 w: f64,
685 h: f64,
686 r_left: f64,
687 r_right: f64,
688}
689
690fn stamp_round_rect(buf: &mut [u8], side: u32, bar: RoundBar, alpha: u8) {
691 if bar.w <= 0.0 || bar.h <= 0.0 {
692 return;
693 }
694 let side_i = side as i32;
695 let min_x = bar.x.floor().max(0.0) as i32;
696 let min_y = bar.y.floor().max(0.0) as i32;
697 let max_x = (bar.x + bar.w).ceil().min(f64::from(side)) as i32;
698 let max_y = (bar.y + bar.h).ceil().min(f64::from(side)) as i32;
699 let target = f64::from(alpha);
700 for py in min_y..max_y {
701 for px in min_x..max_x {
702 if px < 0 || py < 0 || px >= side_i || py >= side_i {
703 continue;
704 }
705 let cover = sample_round_rect(f64::from(px) + 0.5, f64::from(py) + 0.5, &bar);
706 if cover <= 0.0 {
707 continue;
708 }
709 let idx = ((py as u32 * side + px as u32) * 4) as usize;
710 let stamped = (target * cover).round().clamp(0.0, 255.0) as u8;
711 if stamped > buf[idx + 3] {
712 buf[idx] = 0;
713 buf[idx + 1] = 0;
714 buf[idx + 2] = 0;
715 buf[idx + 3] = stamped;
716 }
717 }
718 }
719}
720
721fn sample_round_rect(px: f64, py: f64, bar: &RoundBar) -> f64 {
722 (0.5 - sd_round_box(px, py, bar)).clamp(0.0, 1.0)
723}
724
725fn sd_round_box(px: f64, py: f64, bar: &RoundBar) -> f64 {
726 let r = if px < bar.x + bar.w * 0.5 {
727 bar.r_left
728 } else {
729 bar.r_right
730 };
731 let r = r.max(0.0).min(bar.h * 0.5).min(bar.w * 0.5);
732 let cx = bar.x + bar.w * 0.5;
733 let cy = bar.y + bar.h * 0.5;
734 let dx = (px - cx).abs() - (bar.w * 0.5 - r);
735 let dy = (py - cy).abs() - (bar.h * 0.5 - r);
736 let outside = (dx.max(0.0).powi(2) + dy.max(0.0).powi(2)).sqrt();
737 outside + dx.min(0.0).max(dy.min(0.0)) - r
738}
739
740#[cfg(test)]
741mod tests {
742 use super::*;
743 use serde_json::json;
744
745 #[test]
746 fn zero_or_negative_fraction_draws_no_fill_but_a_visible_track() {
747 let empty = bar_fill(100.0, 0.0);
748 assert_eq!(empty.fill_w, 0.0);
749 assert_eq!(empty.remainder_w, 100.0);
750 assert_eq!(empty.divider_x, Some(0.0));
751 assert_eq!(bar_fill(100.0, -0.5).fill_w, 0.0);
752 let nan = bar_fill(100.0, f64::NAN);
754 assert_eq!(nan.remainder_w, 0.0);
755 assert_eq!(nan.divider_x, None);
756 }
757
758 #[test]
759 fn full_fraction_fills_track_with_no_remainder() {
760 let fill = bar_fill(100.0, 1.0);
761 assert_eq!(fill.fill_w, 100.0);
762 assert_eq!(fill.remainder_w, 0.0);
763 assert_eq!(fill.divider_x, None);
764 }
765
766 #[test]
767 fn near_full_keeps_a_visible_tail() {
768 let fill = bar_fill(100.0, 0.97);
769 assert!(fill.fill_w < 100.0);
770 assert!(fill.remainder_w >= 20.0);
771 assert_eq!(fill.divider_x, Some(fill.fill_w));
772 }
773
774 #[test]
775 fn visual_fraction_quantizes_near_full_in_fifteen_percent_steps() {
776 assert!((visual_fraction(0.97) - 0.85).abs() < 0.0001);
777 assert_eq!(visual_fraction(1.0), 1.0);
778 assert_eq!(visual_fraction(0.0), 0.0);
779 assert!((visual_fraction(0.5) - 0.5).abs() < 0.0001);
780 }
781
782 fn sample_payload() -> Value {
783 json!({
784 "entries": [
785 {
786 "id": "anthropic",
787 "display_name": "Claude",
788 "status": "ready",
789 "sections": [
790 {"type": "metric", "label": "Weekly", "percent": 19, "value": "19%"},
791 {"type": "metric", "label": "Session", "percent": 41, "value": "41%"}
792 ]
793 },
794 {
795 "id": "openai",
796 "display_name": "Codex",
797 "status": "ready",
798 "sections": [
799 {"type": "metric", "label": "Codex weekly", "percent": 2, "value": "2%"}
800 ]
801 },
802 {
803 "id": "cursor",
804 "display_name": "Cursor",
805 "status": "error",
806 "sections": [
807 {"type": "metric", "label": "Cursor Models", "percent": 90, "value": "90%"}
808 ]
809 }
810 ]
811 })
812 }
813
814 #[test]
817 fn metric_keys_match_the_popover_fixture() {
818 let fixture: Value =
819 serde_json::from_str(include_str!("../../tests/fixtures/strip_metric_keys.json"))
820 .expect("fixture is JSON");
821 let cases = fixture["cases"].as_array().expect("cases");
822 assert!(!cases.is_empty());
823 for case in cases {
824 let id = case["id"].as_str().unwrap();
825 let label = case["label"].as_str().unwrap();
826 let group = case["group"].as_str().unwrap_or("");
827 assert_eq!(
828 metric_key(id, label, group),
829 case["key"].as_str().unwrap(),
830 "{case}"
831 );
832 }
833 }
834
835 #[test]
839 fn a_starred_supergrok_meter_is_painted() {
840 let payload = serde_json::json!({ "entries": [
841 { "id": "supergrok", "status": "ready", "sections": [
842 { "type": "metric", "label": "Weekly usage", "percent": 3 }
843 ]}
844 ]});
845 let mut stars = Stars::new();
846 stars.insert("supergrok".into(), vec!["metric:Weekly".into()]);
847 let content = content_from_payload(&payload, &stars, &["supergrok".into()]);
848 assert_eq!(content.bars.len(), 1);
849 assert_eq!(content.bars[0].key, "metric:Weekly");
850 }
851
852 #[test]
853 fn empty_stars_fall_back_to_first_two_bounded_metrics() {
854 let content = content_from_payload(&sample_payload(), &Stars::new(), &[]);
855 assert_eq!(content.groups.len(), 2);
856 assert_eq!(content.groups[0].2.len(), 2);
857 assert_eq!(content.groups[0].2[0].key, "metric:Weekly");
858 assert_eq!(content.groups[0].2[1].key, "metric:Session");
859 assert_eq!(content.groups[1].2[0].key, "metric:Codex weekly");
860 assert_eq!(content.bars.len(), 3);
861 assert!(!content.title_line().is_empty());
862 }
863
864 #[test]
865 fn stars_select_named_metrics_and_skip_errors() {
866 let mut stars = Stars::new();
867 stars.insert("anthropic".into(), vec!["metric:Session".into()]);
868 stars.insert("cursor".into(), vec!["metric:Cursor Models".into()]);
869 let content = content_from_payload(&sample_payload(), &stars, &[]);
870 assert_eq!(content.groups.len(), 1);
871 assert_eq!(content.groups[0].2[0].key, "metric:Session");
872 assert_eq!(content.groups[0].2[0].fraction, 0.41);
873 assert_eq!(content.bars.len(), 1);
874 }
875
876 #[test]
877 fn grouped_metric_keys_match_the_popover() {
878 let payload = json!({
879 "entries": [{
880 "id": "antigravity",
881 "display_name": "Antigravity",
882 "status": "ready",
883 "sections": [
884 {"type": "text", "label": "Session", "value": ""},
885 {"type": "metric", "label": "Gemini", "percent": 4, "value": "4%"},
886 {"type": "text", "label": "Weekly", "value": ""},
887 {"type": "metric", "label": "Gemini", "percent": 11, "value": "11%"}
888 ]
889 }]
890 });
891 let content = content_from_payload(&payload, &Stars::new(), &[]);
892 assert_eq!(content.groups[0].2[0].key, "metric:Gemini (Session)");
893 assert_eq!(content.groups[0].2[1].key, "metric:Gemini (Weekly)");
894 }
895
896 #[test]
901 fn metric_level_groups_key_like_positional_ones() {
902 let payload = json!({
903 "entries": [{
904 "id": "anthropic",
905 "display_name": "Claude",
906 "status": "ready",
907 "sections": [
908 {"type": "metric", "label": "Weekly (7d)", "percent": 32, "value": "32%"},
909 {"type": "metric", "label": "ship the release", "percent": 91,
910 "value": "91%", "group": "Sessions"},
911 {"type": "metric", "label": "Grok Build", "percent": 94,
912 "value": "94%", "group": "Breakdown"}
913 ]
914 }]
915 });
916 let mut stars = Stars::new();
917 stars.insert(
918 "anthropic".into(),
919 vec![
920 "metric:ship the release (Sessions)".into(),
921 "metric:Grok Build (Breakdown)".into(),
922 ],
923 );
924 let content = content_from_payload(&payload, &stars, &[]);
925 let bars = &content.groups[0].2;
926 assert_eq!(bars.len(), 2);
927 assert_eq!(bars[0].key, "metric:ship the release (Sessions)");
928 assert_eq!(bars[0].label, "ship the release (Sessions)");
929 assert_eq!(bars[1].key, "metric:Grok Build (Breakdown)");
930 assert_eq!(bars[1].label, "Grok Build (Breakdown)");
931 }
932
933 #[test]
936 fn left_value_is_the_remaining_percent() {
937 let payload = json!({"entries":[{"id":"zai", "status":"ready", "sections":[
938 {"type":"metric", "label":"Weekly", "percent":18, "value":"18%"},
939 {"type":"metric", "label":"Over", "percent":130, "value":"130%"},
940 {"type":"metric", "label":"Credits", "percent":40, "value":"$4 of $10", "headline":"value"},
941 {"type":"metric", "label":"Balance", "value":"$40"}]}]});
942 let (_, _, metrics) = quota_group(&payload, "zai", &[]).unwrap();
943 let left: Vec<Option<&str>> = metrics.iter().map(|m| m.left_value.as_deref()).collect();
944 assert_eq!(left, vec![Some("82%"), Some("0%"), None, None]);
945 }
946
947 #[test]
951 fn quota_group_leaves_out_hidden_metrics() {
952 let payload = json!({"entries":[{"id":"zai", "status":"ready", "sections":[
953 {"type":"metric", "label":"Session (5h)", "percent":0, "value":"0%"},
954 {"type":"metric", "label":"MCP tools (monthly)", "percent":18, "value":"18%"},
955 {"type":"metric", "label":"Glm", "group":"Models", "percent":40, "value":"40%"}]}]});
956 let keys = |hidden: &[&str]| {
957 let hidden: Vec<String> = hidden.iter().map(|k| k.to_string()).collect();
958 quota_group(&payload, "zai", &hidden)
959 .map(|(_, _, metrics)| metrics.into_iter().map(|m| m.key).collect::<Vec<_>>())
960 };
961
962 assert_eq!(
963 keys(&[]),
964 Some(vec![
965 "metric:Session (5h)".to_string(),
966 "metric:MCP tools (monthly)".to_string()
967 ])
968 );
969 assert_eq!(
970 keys(&["metric:MCP tools (monthly)"]),
971 Some(vec!["metric:Session (5h)".to_string()])
972 );
973 assert_eq!(
974 keys(&["metric:Session (5h)", "metric:MCP tools (monthly)"]),
975 Some(vec!["metric:Glm (Models)".to_string()])
976 );
977 assert_eq!(
978 keys(&[
979 "metric:Session (5h)",
980 "metric:MCP tools (monthly)",
981 "metric:Glm (Models)"
982 ]),
983 None
984 );
985 }
986
987 #[test]
990 fn parse_hidden_rows_reads_the_strip_ipc() {
991 let hidden = parse_hidden_rows(&json!({"hidden_rows": {
992 "zai": [" metric:MCP tools (monthly) ", "metric:MCP tools (monthly)", "", 7],
993 "": ["metric:Weekly"],
994 "openai": [],
995 "kimi": "metric:Weekly"
996 }}));
997 assert_eq!(hidden.len(), 1);
998 assert_eq!(
999 hidden["zai"],
1000 vec!["metric:MCP tools (monthly)".to_string()]
1001 );
1002 assert!(parse_hidden_rows(&json!({"stars": {}})).is_empty());
1003 assert!(parse_hidden_rows(&json!({"hidden_rows": ["zai"]})).is_empty());
1004 }
1005
1006 #[test]
1007 fn parse_strip_ipc_caps_stars_at_two_per_provider() {
1008 let value = json!({
1009 "style": "text",
1010 "stars": {
1011 "anthropic": ["metric:Weekly", "metric:Session", "metric:Extra"],
1012 "": ["x"],
1013 "openai": ["metric:Codex weekly"]
1014 }
1015 });
1016 let (style, stars, order) = parse_strip_ipc(&value);
1017 assert_eq!(style, StripStyle::Bars);
1018 assert_eq!(stars["anthropic"].len(), 2);
1019 assert_eq!(stars["openai"], vec!["metric:Codex weekly".to_string()]);
1020 assert!(!stars.contains_key(""));
1021 assert!(order.is_empty());
1022 }
1023
1024 #[test]
1025 fn parse_strip_ipc_preserves_explicit_empty_stars() {
1026 let (_, stars, _) = parse_strip_ipc(&json!({"stars": {
1027 "anthropic": [],
1028 "openai": []
1029 }}));
1030 assert_eq!(stars.len(), 2);
1031 assert!(stars["anthropic"].is_empty());
1032 assert!(content_from_payload(&sample_payload(), &stars, &[]).is_empty());
1033 }
1034
1035 #[test]
1036 fn tooltip_uses_stars_order_and_reading() {
1037 let content = content_from_payload(&sample_payload(), &Stars::new(), &[]);
1038 assert_eq!(
1039 tooltip(&content, UsageReading::Used),
1040 "Claude · 19% 41%\nCodex · 2%"
1041 );
1042 assert_eq!(
1043 tooltip(&content, UsageReading::Left),
1044 "Claude · 81% 59%\nCodex · 98%"
1045 );
1046 assert_eq!(
1047 UsageReading::from_strip_ipc(&json!({"show_as": "left"})),
1048 UsageReading::Left
1049 );
1050 }
1051
1052 #[test]
1053 fn tooltip_sanitizes_untrusted_native_text() {
1054 let payload = json!({"entries": [{
1055 "id": "provider",
1056 "display_name": "Bad\nName",
1057 "status": "ready",
1058 "sections": [{"type": "metric", "label": "Weekly", "percent": 9, "value": "9%\r"}]
1059 }]});
1060 let content = content_from_payload(&payload, &Stars::new(), &[]);
1061 assert_eq!(tooltip(&content, UsageReading::Used), "BadName · 9%");
1062 assert_eq!(
1063 tooltip(
1064 &StripContent {
1065 groups: vec![],
1066 bars: vec![]
1067 },
1068 UsageReading::Used
1069 ),
1070 "AI Usage"
1071 );
1072 }
1073
1074 #[test]
1075 fn utf16_truncation_never_splits_a_surrogate_pair() {
1076 assert_eq!(truncate_utf16("a😀b", 3), "a😀");
1077 assert_eq!(truncate_utf16("a😀b", 2), "a");
1078 assert_eq!(truncate_utf16("abc", 127), "abc");
1079 }
1080
1081 #[test]
1082 fn strip_follows_popover_card_order() {
1083 let mut stars = Stars::new();
1084 stars.insert("anthropic".into(), vec!["metric:Weekly".into()]);
1085 stars.insert("openai".into(), vec!["metric:Codex weekly".into()]);
1086 let content = content_from_payload(
1087 &sample_payload(),
1088 &stars,
1089 &["openai".into(), "anthropic".into()],
1090 );
1091 assert_eq!(content.groups[0].0, "openai");
1092 assert_eq!(content.groups[1].0, "anthropic");
1093 assert_eq!(content.bars[0].provider_id, "openai");
1094 assert_eq!(content.bars[1].provider_id, "anthropic");
1095 }
1096
1097 #[test]
1098 fn bars_rgba_is_square_black_and_anti_aliased() {
1099 let bytes = bars_rgba(&[0.4, 0.97], BARS_PIXEL_SIDE);
1100 assert_eq!(
1101 bytes.len(),
1102 (BARS_PIXEL_SIDE * BARS_PIXEL_SIDE * 4) as usize
1103 );
1104 assert_eq!(bytes[3], 0, "top-left stays transparent");
1105 let last = bytes.len() - 1;
1106 assert_eq!(bytes[last], 0, "bottom-right stays transparent");
1107 let mut inked = 0usize;
1108 let mut soft = 0usize;
1109 for pixel in bytes.as_chunks::<4>().0 {
1110 if pixel[3] > 0 {
1111 inked += 1;
1112 assert_eq!(&pixel[..3], &[0, 0, 0]);
1113 }
1114 if pixel[3] > 0 && pixel[3] < 255 {
1115 soft += 1;
1116 }
1117 }
1118 assert!(inked > 40, "glyph too sparse: {inked}");
1119 assert!(soft > 0, "expected anti-aliased edges");
1120 }
1121
1122 #[test]
1123 fn empty_fractions_are_fully_transparent() {
1124 let bytes = bars_rgba(&[], BARS_PIXEL_SIDE);
1125 assert!(bytes.as_chunks::<4>().0.iter().all(|p| p[3] == 0));
1126 }
1127
1128 #[test]
1133 fn empty_bars_keep_a_visible_capsule() {
1134 let bytes = bars_rgba(&[0.0, 0.5], BARS_PIXEL_SIDE);
1135 let side = BARS_PIXEL_SIDE as usize;
1136 let layout = bars_layout(2, f64::from(BARS_PIXEL_SIDE));
1137 let y = (layout.y_offset + 1.0 + layout.track_h / 2.0) as usize;
1138 let mut peak = 0u8;
1139 let mut visible_cols = 0usize;
1140 for x in 0..side {
1141 let alpha = bytes[(y * side + x) * 4 + 3];
1142 peak = peak.max(alpha);
1143 if alpha >= 61 {
1144 visible_cols += 1;
1145 }
1146 }
1147 assert_eq!(peak, 61, "empty row peaks at the remainder alpha");
1148 assert!(
1149 visible_cols > side / 2,
1150 "capsule spans the track: {visible_cols}"
1151 );
1152 }
1153
1154 #[test]
1155 fn bars_rgba_at_each_status_item_scale() {
1156 for scale in BARS_SCALES {
1157 let side = BARS_POINT_SIDE * scale;
1158 let bytes = bars_rgba(&[0.4, 0.97], side);
1159 assert_eq!(bytes.len(), (side * side * 4) as usize, "scale {scale}");
1160 assert!(
1161 bytes.as_chunks::<4>().0.iter().any(|p| p[3] > 0),
1162 "scale {scale} produced an empty glyph"
1163 );
1164 }
1165 }
1166}