wisp/components/
context_bar.rs1use tui::{Color, Theme};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub struct ContextUsageDisplay {
6 pub used_tokens: u32,
7 pub limit_tokens: u32,
8}
9
10impl ContextUsageDisplay {
11 pub fn new(used_tokens: u32, limit_tokens: u32) -> Self {
12 Self { used_tokens, limit_tokens }
13 }
14
15 pub fn used_ratio(&self) -> f64 {
16 if self.limit_tokens == 0 {
17 return 0.0;
18 }
19 (f64::from(self.used_tokens) / f64::from(self.limit_tokens)).clamp(0.0, 1.0)
20 }
21}
22
23pub(crate) fn slot_bar(filled: usize, total: usize) -> String {
27 let slots: String = (0..total).map(|i| if i < filled { '■' } else { '·' }).collect();
28 format!("[{slots}]")
29}
30
31pub(crate) fn context_bar(usage: ContextUsageDisplay) -> String {
38 const TOTAL: u32 = 3;
39 let filled = (usage.used_tokens.saturating_mul(TOTAL) + usage.limit_tokens / 2) / usage.limit_tokens.max(1);
40 let filled = (filled as usize).min(TOTAL as usize);
41 format!(
42 "ctx {} {} / {}",
43 slot_bar(filled, TOTAL as usize),
44 format_tokens(usage.used_tokens),
45 format_tokens(usage.limit_tokens)
46 )
47}
48
49pub(crate) fn context_color(usage: ContextUsageDisplay, theme: &Theme) -> Color {
56 let used_pct = usage.used_ratio() * 100.0;
57 if used_pct >= 86.0 {
58 theme.error()
59 } else if used_pct >= 71.0 {
60 theme.warning()
61 } else {
62 theme.text_secondary()
63 }
64}
65
66pub(crate) fn format_tokens(n: u32) -> String {
68 match n {
69 n if n < 1_000 => n.to_string(),
70 n if n < 1_000_000 => format_with_unit(f64::from(n) / 1_000.0, "k"),
71 n => format_with_unit(f64::from(n) / 1_000_000.0, "M"),
72 }
73}
74
75fn format_with_unit(value: f64, unit: &str) -> String {
76 let rounded_one = (value * 10.0).round() / 10.0;
77 if (rounded_one - rounded_one.trunc()).abs() < f64::EPSILON {
78 format!("{rounded_one:.0}{unit}")
79 } else {
80 format!("{rounded_one:.1}{unit}")
81 }
82}
83
84#[cfg(test)]
85mod tests {
86 use super::*;
87
88 fn usage(used: u32, limit: u32) -> ContextUsageDisplay {
89 ContextUsageDisplay::new(used, limit)
90 }
91
92 #[test]
93 fn slot_bar_empty() {
94 assert_eq!(slot_bar(0, 3), "[···]");
95 }
96
97 #[test]
98 fn slot_bar_partial() {
99 assert_eq!(slot_bar(2, 3), "[■■·]");
100 }
101
102 #[test]
103 fn slot_bar_full() {
104 assert_eq!(slot_bar(3, 3), "[■■■]");
105 }
106
107 #[test]
108 fn bar_full() {
109 assert_eq!(context_bar(usage(200_000, 200_000)), "ctx [■■■] 200k / 200k");
110 }
111
112 #[test]
113 fn bar_empty() {
114 assert_eq!(context_bar(usage(0, 200_000)), "ctx [···] 0 / 200k");
115 }
116
117 #[test]
118 fn bar_low() {
119 assert_eq!(context_bar(usage(1_200, 200_000)), "ctx [···] 1.2k / 200k");
120 }
121
122 #[test]
123 fn bar_half() {
124 assert_eq!(context_bar(usage(100_000, 200_000)), "ctx [■■·] 100k / 200k");
125 }
126
127 #[test]
128 fn bar_near_full() {
129 assert_eq!(context_bar(usage(190_000, 200_000)), "ctx [■■■] 190k / 200k");
130 }
131
132 #[test]
133 fn bar_fills_with_usage() {
134 let empty = context_bar(usage(0, 200_000));
135 let half = context_bar(usage(100_000, 200_000));
136 let full = context_bar(usage(200_000, 200_000));
137 assert!(empty.contains("[···]"));
138 assert!(half.contains("[■■·]"));
139 assert!(full.contains("[■■■]"));
140 }
141
142 #[test]
143 fn color_tiers() {
144 let theme = Theme::default();
145 assert_eq!(context_color(usage(0, 200_000), &theme), theme.text_secondary());
146 assert_eq!(context_color(usage(140_000, 200_000), &theme), theme.text_secondary());
147 assert_eq!(context_color(usage(142_000, 200_000), &theme), theme.warning());
148 assert_eq!(context_color(usage(170_000, 200_000), &theme), theme.warning());
149 assert_eq!(context_color(usage(172_000, 200_000), &theme), theme.error());
150 assert_eq!(context_color(usage(200_000, 200_000), &theme), theme.error());
151 }
152
153 #[test]
154 fn format_tokens_examples() {
155 assert_eq!(format_tokens(0), "0");
156 assert_eq!(format_tokens(999), "999");
157 assert_eq!(format_tokens(1_000), "1k");
158 assert_eq!(format_tokens(1_200), "1.2k");
159 assert_eq!(format_tokens(12_000), "12k");
160 assert_eq!(format_tokens(150_000), "150k");
161 assert_eq!(format_tokens(1_200_000), "1.2M");
162 }
163}