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